Merge 'webrtc' into 'dev/staging_webrtc'

This commit is contained in:
TheArcaneBrony
2022-08-27 07:07:05 +02:00
parent aabeffdaff
commit feca7a5d62
95 changed files with 11815 additions and 195 deletions

13
.babelrc Normal file
View File

@@ -0,0 +1,13 @@
{
"plugins": [
[
"transform-imports",
{
"my-library": {
"transform": "../../path/to/transform.js",
"preventFullImport": true
}
}
]
]
}

26
.swcrc Normal file
View File

@@ -0,0 +1,26 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"paths": {
"@fosscord/api*": ["./src/api/index"],
"@fosscord/gateway*": ["./src/gateway/index"],
"@fosscord/cdn*": ["./src/cdn/index"],
"@fosscord/util*": ["./src/util/index"],
"@fosscord/webrtc*": ["./src/webrtc/index"]
},
"target": "es5",
"parser": {
"syntax": "typescript",
"decorators": true,
"dynamicImport": true
},
"transform": {
"legacyDecorator": false,
"decoratorMetadata": true
}
},
"sourceMaps": true,
"module": {
"type": "commonjs"
}
}

13
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build",
"group": "build",
"problemMatcher": [],
"label": "npm: build",
"detail": "node scripts/build.js swc"
}
]
}

View File

@@ -0,0 +1,74 @@
(this.webpackChunkdiscord_app = this.webpackChunkdiscord_app || []).push([
[[228974]],
{
632540: (module, exports, req) => {
window.find = (filter, options = {}) => {
const { cacheOnly = false } = options;
for (let i in req.c) {
if (req.c.hasOwnProperty(i)) {
let m = req.c[i].exports;
if (m && m.__esModule && m.default && filter(m.default)) return m.default;
if (m && filter(m)) return m;
}
}
if (cacheOnly) {
console.warn("Cannot find loaded module in cache");
return null;
}
console.warn("Cannot find loaded module in cache. Loading all modules may have unexpected side effects");
for (let i = 0; i < req.m.length; ++i) {
let m = req(i);
if (m && m.__esModule && m.default && filter(m.default)) return m.default;
if (m && filter(m)) return m;
}
console.warn("Cannot find module");
return null;
};
window.findByUniqueProperties = (propNames, options) =>
find((module) => propNames.every((prop) => module[prop] !== undefined), options);
window.findByDisplayName = (displayName, options) => find((module) => module.displayName === displayName, options);
window.req = req;
init();
}
},
(t) => t(632540)
]);
function retry(callback) {
return new Promise((resolve) => {
const interval = setInterval(() => {
const mod = callback();
if (!mod) return;
clearInterval(interval);
resolve(mod);
}, 50);
});
}
async function init() {
const SDP = await retry(() => findByUniqueProperties(["truncateSDP"]));
const StringManipulator = findByUniqueProperties(["uniq"]);
const truncateSDP = SDP.truncateSDP;
SDP.truncateSDP = (e) => {
const result = truncateSDP(e);
const i = result.codecs.find((x) => x.name === "VP8");
const a = new RegExp("^a=ice|a=extmap|opus|VP8|fingerprint|" + i?.rtxPayloadType + " rtx", "i");
return {
sdp: StringManipulator(e)
.split(/\r\n/)
.filter(function (e) {
return a.test(e);
})
.uniq()
.join("\n"),
codecs: result.codecs
};
};
// SDP.generateUnifiedSessionDescription = (e) => {
// console.log(e);
// return new RTCSessionDescription({ sdp: e.baseSDP.replace(/sendonly/g, "recvonly"), type: "answer" });
// };
}

420
assets/sdp.json Normal file
View File

@@ -0,0 +1,420 @@
{
"version": 0,
"streams": [],
"medias": [
{
"id": "0",
"type": "audio",
"direction": "sendrecv",
"codecs": [
{
"codec": "opus",
"type": 111,
"channels": 2,
"params": {
"minptime": "10",
"useinbandfec": "1"
},
"rtcpfbs": [
{
"id": "transport-cc"
}
]
}
],
"extensions": {
"1": "urn:ietf:params:rtp-hdrext:ssrc-audio-level",
"2": "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",
"3": "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01",
"4": "urn:ietf:params:rtp-hdrext:sdes:mid"
}
},
{
"id": "1",
"type": "video",
"direction": "sendrecv",
"codecs": [
{
"codec": "VP8",
"type": 96,
"rtx": 97,
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "VP9",
"type": 98,
"rtx": 99,
"params": {
"profile-id": "0"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "VP9",
"type": 100,
"rtx": 101,
"params": {
"profile-id": "2"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "VP9",
"type": 102,
"rtx": 122,
"params": {
"profile-id": "1"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "H264",
"type": 127,
"rtx": 121,
"params": {
"level-asymmetry-allowed": "1",
"packetization-mode": "1",
"profile-level-id": "42001f"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "H264",
"type": 125,
"rtx": 107,
"params": {
"level-asymmetry-allowed": "1",
"packetization-mode": "0",
"profile-level-id": "42001f"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "H264",
"type": 108,
"rtx": 109,
"params": {
"level-asymmetry-allowed": "1",
"packetization-mode": "1",
"profile-level-id": "42e01f"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "H264",
"type": 124,
"rtx": 120,
"params": {
"level-asymmetry-allowed": "1",
"packetization-mode": "0",
"profile-level-id": "42e01f"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "H264",
"type": 123,
"rtx": 119,
"params": {
"level-asymmetry-allowed": "1",
"packetization-mode": "1",
"profile-level-id": "4d001f"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "H264",
"type": 35,
"rtx": 36,
"params": {
"level-asymmetry-allowed": "1",
"packetization-mode": "0",
"profile-level-id": "4d001f"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "H264",
"type": 37,
"rtx": 38,
"params": {
"level-asymmetry-allowed": "1",
"packetization-mode": "1",
"profile-level-id": "f4001f"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "H264",
"type": 39,
"rtx": 40,
"params": {
"level-asymmetry-allowed": "1",
"packetization-mode": "0",
"profile-level-id": "f4001f"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
},
{
"codec": "H264",
"type": 114,
"rtx": 115,
"params": {
"level-asymmetry-allowed": "1",
"packetization-mode": "1",
"profile-level-id": "64001f"
},
"rtcpfbs": [
{
"id": "goog-remb"
},
{
"id": "transport-cc"
},
{
"id": "ccm",
"params": ["fir"]
},
{
"id": "nack"
},
{
"id": "nack",
"params": ["pli"]
}
]
}
],
"extensions": {
"2": "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",
"3": "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01",
"4": "urn:ietf:params:rtp-hdrext:sdes:mid",
"5": "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay",
"6": "http://www.webrtc.org/experiments/rtp-hdrext/video-content-type",
"7": "http://www.webrtc.org/experiments/rtp-hdrext/video-timing",
"8": "http://www.webrtc.org/experiments/rtp-hdrext/color-space",
"10": "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
"11": "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
"13": "urn:3gpp:video-orientation",
"14": "urn:ietf:params:rtp-hdrext:toffset"
}
}
],
"candidates": []
}

View File

@@ -16,3 +16,5 @@
| DB_VERBOSE | any | Log database queries, enabled if defined |
| DB_MIGRATE | any | Exit fosscord after connecting to and migrating database, used internally |
| LOG_INVALID_BODY | any | Log request method, path and body if invalid |
| WEBRTC_PORT_RANGE | min-max | Minimum and maximum port for the WebRTC server |
| PUBLIC_IP | string | Public ip of the WebRTC server |

View File

@@ -0,0 +1,5 @@
#rm -rf dist/
#mkdir dist
rm -rfv *.js *.js.map
ln -s ../../bundle/node_modules node_modules
tsc -p .

View File

@@ -0,0 +1,7 @@
import { Plugin } from "@fosscord/util";
export default class TestPlugin extends Plugin {
onPluginLoaded(): void {
console.log("Hello from test plugin! IT WORKS!!!!!!!");
}
}

View File

@@ -0,0 +1,7 @@
{
"id": "example-plugin",
"name": "Fosscord example plugin",
"authors": ["The Arcane Brony"],
"repository": "https://github.com/fosscord/fosscord-server",
"license": ""
}

41
scripts/db_migrations.sh Normal file
View File

@@ -0,0 +1,41 @@
#!/bin/sh
if [ ! -z "$1" ]
then
FILENAME="$1"
echo "Using filename: $FILENAME"
else
read -p "Enter migration filename: " FILENAME
fi
[ -f ".env" ] && (
mv .env .env.tmp 2>/dev/null
source .env.tmp 2>/dev/null
)
npm run build clean logerrors pretty-errors
make_migration() {
echo "Creating migrations for $2"
mkdir "src/util/migrations/$2" 2>/dev/null
# npm run build clean logerrors pretty-errors
THREADS=1 DATABASE="$1" DB_MIGRATE=a npm run start:bundle
THREADS=1 DATABASE="$1" DB_MIGRATE=a npx typeorm-ts-node-commonjs migration:generate "src/util/migrations/$2/$FILENAME" -d src/util/util/Database.ts -p
#npm run build clean logerrors pretty-errors
#THREADS=1 DATABASE="$1" DB_MIGRATE=a npm run start:bundle
}
npm i sqlite3
make_migration "database.db" "sqlite"
[ -z "$FC_DB_POSTGRES" ] || (
npm i pg
make_migration "$FC_DB_POSTGRES" "postgres"
)
[ -z "$FC_DB_MARIADB" ] || (
npm i mysql2
make_migration "$FC_DB_MARIADB" "mariadb"
)
[ -f ".env.tmp" ] && mv .env.tmp .env 2>/dev/null

View File

@@ -0,0 +1,172 @@
v=0
o=- 2111259993848660015 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
a=extmap-allow-mixed
a=msid-semantic: WMS MwWzwzG0U0kdKhH0cMlNMyuU9Q7BmjjLYpQl
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:KRoB
a=ice-pwd:k44fKSe3PpPYjljQaY2YObSu
a=ice-options:trickle
a=fingerprint:sha-256 E2:B8:09:30:0F:80:7F:97:82:2B:A9:97:AD:67:8B:D1:C8:48:99:5D:26:ED:2C:B2:4D:41:E6:5E:62:FC:99:C1
a=setup:actpass
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=sendrecv
a=msid:MwWzwzG0U0kdKhH0cMlNMyuU9Q7BmjjLYpQl 8314115b-9e3f-49eb-9183-22146b9bfe2c
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:63 red/48000/2
a=fmtp:63 111/111
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
a=ssrc:1910637016 cname:211W6KVUXfLFKBkS
a=ssrc:1910637016 msid:MwWzwzG0U0kdKhH0cMlNMyuU9Q7BmjjLYpQl 8314115b-9e3f-49eb-9183-22146b9bfe2c
m=video 9 UDP/TLS/RTP/SAVPF 96 97 127 121 125 107 108 109 124 120 123 119 35 36 41 42 98 99 100 101 114 115 116 117 118
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:KRoB
a=ice-pwd:k44fKSe3PpPYjljQaY2YObSu
a=ice-options:trickle
a=fingerprint:sha-256 E2:B8:09:30:0F:80:7F:97:82:2B:A9:97:AD:67:8B:D1:C8:48:99:5D:26:ED:2C:B2:4D:41:E6:5E:62:FC:99:C1
a=setup:actpass
a=mid:1
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:13 urn:3gpp:video-orientation
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=sendrecv
a=msid:MwWzwzG0U0kdKhH0cMlNMyuU9Q7BmjjLYpQl 3cc31637-6e0f-4b57-8480-35c04f86bba3
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:127 H264/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=127
a=rtpmap:125 H264/90000
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 transport-cc
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtpmap:108 H264/90000
a=rtcp-fb:108 goog-remb
a=rtcp-fb:108 transport-cc
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtpmap:124 H264/90000
a=rtcp-fb:124 goog-remb
a=rtcp-fb:124 transport-cc
a=rtcp-fb:124 ccm fir
a=rtcp-fb:124 nack
a=rtcp-fb:124 nack pli
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=124
a=rtpmap:123 H264/90000
a=rtcp-fb:123 goog-remb
a=rtcp-fb:123 transport-cc
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 nack
a=rtcp-fb:123 nack pli
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
a=rtpmap:119 rtx/90000
a=fmtp:119 apt=123
a=rtpmap:35 H264/90000
a=rtcp-fb:35 goog-remb
a=rtcp-fb:35 transport-cc
a=rtcp-fb:35 ccm fir
a=rtcp-fb:35 nack
a=rtcp-fb:35 nack pli
a=fmtp:35 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
a=rtpmap:36 rtx/90000
a=fmtp:36 apt=35
a=rtpmap:41 AV1/90000
a=rtcp-fb:41 goog-remb
a=rtcp-fb:41 transport-cc
a=rtcp-fb:41 ccm fir
a=rtcp-fb:41 nack
a=rtcp-fb:41 nack pli
a=rtpmap:42 rtx/90000
a=fmtp:42 apt=41
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 profile-id=0
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP9/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=fmtp:100 profile-id=2
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:114 H264/90000
a=rtcp-fb:114 goog-remb
a=rtcp-fb:114 transport-cc
a=rtcp-fb:114 ccm fir
a=rtcp-fb:114 nack
a=rtcp-fb:114 nack pli
a=fmtp:114 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
a=rtpmap:115 rtx/90000
a=fmtp:115 apt=114
a=rtpmap:116 red/90000
a=rtpmap:117 rtx/90000
a=fmtp:117 apt=116
a=rtpmap:118 ulpfec/90000
a=ssrc-group:FID 1863819511 3594276773
a=ssrc:1863819511 cname:211W6KVUXfLFKBkS
a=ssrc:1863819511 msid:MwWzwzG0U0kdKhH0cMlNMyuU9Q7BmjjLYpQl 3cc31637-6e0f-4b57-8480-35c04f86bba3
a=ssrc:3594276773 cname:211W6KVUXfLFKBkS
a=ssrc:3594276773 msid:MwWzwzG0U0kdKhH0cMlNMyuU9Q7BmjjLYpQl 3cc31637-6e0f-4b57-8480-35c04f86bba3

View File

@@ -0,0 +1,19 @@
a=extmap-allow-mixed
a=ice-ufrag:Ets9
a=ice-pwd:CKGC4jufinWBOiKgn9iUji0l
a=ice-options:trickle
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=rtpmap:111 opus/48000/2
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=rtpmap:96 VP8/90000
a=rtpmap:97 rtx/90000

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
v=0
o=- 2875442641301787241 4 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
a=extmap-allow-mixed
a=msid-semantic: WMS
m=audio 58625 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126
c=IN IP4 192.168.178.21
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:2188944245 1 udp 2122262783 2a02:2455:16e0:b800:2948:7f13:404e:79d9 58131 typ host generation 0 network-id 2 network-cost 10
a=candidate:1967680543 1 udp 2122194687 192.168.178.21 58625 typ host generation 0 network-id 1 network-cost 10
a=candidate:3439026053 1 tcp 1518283007 2a02:2455:16e0:b800:2948:7f13:404e:79d9 9 typ host tcptype active generation 0 network-id 2 network-cost 10
a=candidate:1003196655 1 tcp 1518214911 192.168.178.21 9 typ host tcptype active generation 0 network-id 1 network-cost 10
a=ice-ufrag:4vO7
a=ice-pwd:SFNoE73NpBg3ABrSge26wJwo
a=ice-options:trickle
a=fingerprint:sha-256 2C:E3:F2:AE:F3:5B:69:32:A9:14:33:40:B3:A8:25:BE:67:A2:58:94:65:0C:9D:55:87:28:94:B6:DC:81:8F:63
a=setup:actpass
a=mid:0
b=AS:96
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=sendrecv
a=msid:- 7ffd5d58-a23a-4d2e-ba03-c5c567fc5608
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:63 red/48000/2
a=fmtp:63 111/111
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
a=ssrc:119334120 cname:rVy8IByaD2Z8419E
a=ssrc:119334120 msid:- 7ffd5d58-a23a-4d2e-ba03-c5c567fc5608

View File

@@ -0,0 +1,8 @@
m=audio 50025 ICE/SDP
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
c=IN IP4 66.22.238.141
a=rtcp:50025
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host

View File

@@ -0,0 +1,468 @@
v=0
o=- 1420070400000 0 IN IP4 127.0.0.1
s=-
t=0 0
a=msid-semantic: WMS *
a=group:BUNDLE 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
m=audio 50025 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.238.141
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=rtcp:50025
a=rtcp-fb:111 transport-cc
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=setup:passive
a=mid:0
a=maxptime:60
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=video 50025 UDP/TLS/RTP/SAVPF 127 121
c=IN IP4 66.22.238.141
a=rtpmap:127 H264/90000
a=rtpmap:121 rtx/90000
a=fmtp:127 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:121 apt=127
a=rtcp:50025
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=setup:passive
a=mid:1
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=audio 50025 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.238.141
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=rtcp:50025
a=rtcp-fb:111 transport-cc
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=setup:passive
a=mid:2
a=maxptime:60
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=audio 50025 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.238.141
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=rtcp:50025
a=rtcp-fb:111 transport-cc
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=setup:passive
a=mid:3
a=maxptime:60
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=audio 50025 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.238.141
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=rtcp:50025
a=rtcp-fb:111 transport-cc
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=setup:passive
a=mid:4
a=maxptime:60
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=audio 50025 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.238.141
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=rtcp:50025
a=rtcp-fb:111 transport-cc
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=setup:passive
a=mid:5
a=maxptime:60
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=audio 50025 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.238.141
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=rtcp:50025
a=rtcp-fb:111 transport-cc
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=setup:passive
a=mid:6
a=maxptime:60
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=audio 50025 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.238.141
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=rtcp:50025
a=rtcp-fb:111 transport-cc
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=setup:passive
a=mid:7
a=maxptime:60
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=audio 50025 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.238.141
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=rtcp:50025
a=rtcp-fb:111 transport-cc
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=setup:passive
a=mid:8
a=maxptime:60
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=audio 50025 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.238.141
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=rtcp:50025
a=rtcp-fb:111 transport-cc
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=setup:passive
a=mid:9
a=maxptime:60
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=audio 50025 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.238.141
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=rtcp:50025
a=rtcp-fb:111 transport-cc
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=setup:passive
a=mid:10
a=maxptime:60
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=audio 50025 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.238.141
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=rtcp:50025
a=rtcp-fb:111 transport-cc
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=setup:passive
a=mid:11
a=maxptime:60
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=video 50025 UDP/TLS/RTP/SAVPF 127 121
c=IN IP4 66.22.238.141
a=rtpmap:127 H264/90000
a=rtpmap:121 rtx/90000
a=fmtp:127 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:121 apt=127
a=rtcp:50025
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=setup:passive
a=mid:12
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=video 50025 UDP/TLS/RTP/SAVPF 127 121
c=IN IP4 66.22.238.141
a=rtpmap:127 H264/90000
a=rtpmap:121 rtx/90000
a=fmtp:127 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:121 apt=127
a=rtcp:50025
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=setup:passive
a=mid:13
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=video 50025 UDP/TLS/RTP/SAVPF 127 121
c=IN IP4 66.22.238.141
a=rtpmap:127 H264/90000
a=rtpmap:121 rtx/90000
a=fmtp:127 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:121 apt=127
a=rtcp:50025
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=setup:passive
a=mid:14
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=video 50025 UDP/TLS/RTP/SAVPF 127 121
c=IN IP4 66.22.238.141
a=rtpmap:127 H264/90000
a=rtpmap:121 rtx/90000
a=fmtp:127 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:121 apt=127
a=rtcp:50025
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=setup:passive
a=mid:15
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=video 50025 UDP/TLS/RTP/SAVPF 127 121
c=IN IP4 66.22.238.141
a=rtpmap:127 H264/90000
a=rtpmap:121 rtx/90000
a=fmtp:127 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:121 apt=127
a=rtcp:50025
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=setup:passive
a=mid:16
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=video 50025 UDP/TLS/RTP/SAVPF 127 121
c=IN IP4 66.22.238.141
a=rtpmap:127 H264/90000
a=rtpmap:121 rtx/90000
a=fmtp:127 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:121 apt=127
a=rtcp:50025
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=setup:passive
a=mid:17
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=video 50025 UDP/TLS/RTP/SAVPF 127 121
c=IN IP4 66.22.238.141
a=rtpmap:127 H264/90000
a=rtpmap:121 rtx/90000
a=fmtp:127 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:121 apt=127
a=rtcp:50025
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=setup:passive
a=mid:18
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=video 50025 UDP/TLS/RTP/SAVPF 127 121
c=IN IP4 66.22.238.141
a=rtpmap:127 H264/90000
a=rtpmap:121 rtx/90000
a=fmtp:127 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:121 apt=127
a=rtcp:50025
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=setup:passive
a=mid:19
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=video 50025 UDP/TLS/RTP/SAVPF 127 121
c=IN IP4 66.22.238.141
a=rtpmap:127 H264/90000
a=rtpmap:121 rtx/90000
a=fmtp:127 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:121 apt=127
a=rtcp:50025
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=setup:passive
a=mid:20
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux
m=video 50025 UDP/TLS/RTP/SAVPF 127 121
c=IN IP4 66.22.238.141
a=rtpmap:127 H264/90000
a=rtpmap:121 rtx/90000
a=fmtp:127 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:121 apt=127
a=rtcp:50025
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=setup:passive
a=mid:21
a=inactive
a=ice-ufrag:VHK9
a=ice-pwd:FNTPHMd2XNegYm+xwcWJWK
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=candidate:1 1 UDP 4261412862 66.22.238.141 50025 typ host
a=rtcp-mux

33
scripts/webrtc/sdp.js Normal file
View File

@@ -0,0 +1,33 @@
const { writeFileSync } = require("fs");
const SemanticSDP = require("semantic-sdp");
var data = `m=audio
a=extmap-allow-mixed
a=ice-ufrag:Ets9
a=ice-pwd:CKGC4jufinWBOiKgn9iUji0l
a=ice-options:trickle
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=rtpmap:111 opus/48000/2
a=fingerprint:sha-256 2C:E3:F2:AE:F3:5B:69:32:A9:14:33:40:B3:A8:25:BE:67:A2:58:94:65:0C:9D:55:87:28:94:B6:DC:81:8F:63
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=rtpmap:96 VP8/90000
a=rtpmap:97 rtx/90000
`;
const sdp = SemanticSDP.SDPInfo.parse(data);
writeFileSync("sdp.json", JSON.stringify(sdp.plain(), null, 2));
const x = require("./sdp.json");
const y = SemanticSDP.SDPInfo.expand(x);
console.log(y);

View File

@@ -5,6 +5,7 @@ import * as Api from "@fosscord/api";
import { CDNServer } from "@fosscord/cdn";
import * as Gateway from "@fosscord/gateway";
import { Config, getOrInitialiseDatabase } from "@fosscord/util";
import * as WebRTC from "@fosscord/webrtc";
import * as Sentry from "@sentry/node";
import * as Tracing from "@sentry/tracing";
import express from "express";
@@ -18,12 +19,10 @@ const port = Number(process.env.PORT) || 3001;
const production = process.env.NODE_ENV == "development" ? false : true;
server.on("request", app);
// @ts-ignore
const api = new Api.FosscordServer({ server, port, production, app });
// @ts-ignore
const cdn = new CDNServer({ server, port, production, app });
// @ts-ignore
const gateway = new Gateway.Server({ server, port, production });
const webrtc = new WebRTC.Server({ server, port, production });
//this is what has been added for the /stop API route
process.on("SIGTERM", () => {
@@ -52,7 +51,7 @@ async function main() {
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
}
await Promise.all([api.start(), cdn.start(), gateway.start()]);
await Promise.all([api.start(), cdn.start(), gateway.start(), webrtc.start()]);
if (Config.get().sentry.enabled) {
app.use(Sentry.Handlers.errorHandler());
app.use(function onError(err: any, req: any, res: any, next: any) {

View File

@@ -13,7 +13,7 @@ export async function getVoiceRegions(ipAddress: string, vip: boolean) {
for (let ar of availableRegions) {
//TODO the endpoint location should be saved in the database if not already present to prevent IPAnalysis call
const dist = distanceBetweenLocations(clientIpAnalysis, ar.location || (await IPAnalysis(ar.endpoint)));
const dist = distanceBetweenLocations(clientIpAnalysis, ar.location || (await IPAnalysis(ar.endpoint!)));
if (dist < min) {
min = dist;

View File

@@ -23,6 +23,7 @@ export class Server {
}
this.server.on("upgrade", (request, socket, head) => {
if (request.url?.includes("voice")) return;
// @ts-ignore
this.ws.handleUpgrade(request, socket, head, (socket) => {
this.ws.emit("connection", socket, request);

View File

@@ -2,7 +2,7 @@ import { WebSocket } from "@fosscord/gateway";
import { emitEvent, PresenceUpdateEvent, PrivateSessionProjection, Session, SessionsReplace, User } from "@fosscord/util";
export async function Close(this: WebSocket, code: number, reason: string) {
console.log("[WebSocket] closed", code, reason);
console.log("[Gateway] closed", code, reason);
if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout);
if (this.readyTimeout) clearTimeout(this.readyTimeout);
this.deflate?.close();

View File

@@ -3,7 +3,7 @@ import { IncomingMessage } from "http";
import { URL } from "url";
import WS from "ws";
import { createDeflate } from "zlib";
import { CLOSECODES, OPCODES } from "../util/Constants";
import { CloseCodes, GatewayOPCodes } from "../util/Constants";
import { setHeartbeat } from "../util/Heartbeat";
import { Send } from "../util/Send";
import { Close } from "./Close";
@@ -35,7 +35,7 @@ export async function Connection(this: WS.Server, socket: WebSocket, request: In
"pong",
"unexpected-response"
].forEach((x) => {
socket.on(x, (y) => console.log(x, y));
socket.on(x, (y) => console.log("[Gateway]", x, y));
});
console.log(`[Gateway] Connections: ${this.clients.size}`);
@@ -47,17 +47,17 @@ export async function Connection(this: WS.Server, socket: WebSocket, request: In
if (socket.encoding === "etf" && erlpack) {
throw new Error("Erlpack is not installed: 'npm i @yukikaze-bot/erlpack'");
}
return socket.close(CLOSECODES.Decode_error);
return socket.close(CloseCodes.Decode_error);
}
// @ts-ignore
socket.version = Number(searchParams.get("version")) || 8;
if (socket.version != 8) return socket.close(CLOSECODES.Invalid_API_version);
if (socket.version != 8) return socket.close(CloseCodes.Invalid_API_version);
// @ts-ignore
socket.compress = searchParams.get("compress") || "";
if (socket.compress) {
if (socket.compress !== "zlib-stream") return socket.close(CLOSECODES.Decode_error);
if (socket.compress !== "zlib-stream") return socket.close(CloseCodes.Decode_error);
socket.deflate = createDeflate({ chunkSize: 65535 });
socket.deflate.on("data", (chunk) => socket.send(chunk));
}
@@ -69,18 +69,18 @@ export async function Connection(this: WS.Server, socket: WebSocket, request: In
setHeartbeat(socket);
socket.readyTimeout = setTimeout(() => {
return socket.close(CloseCodes.Session_timed_out);
}, 1000 * 30);
await Send(socket, {
op: OPCODES.Hello,
op: GatewayOPCodes.Hello,
d: {
heartbeat_interval: 1000 * 30
}
});
socket.readyTimeout = setTimeout(() => {
return socket.close(CLOSECODES.Session_timed_out);
}, 1000 * 30);
} catch (error) {
console.error(error);
return socket.close(CLOSECODES.Unknown_error);
return socket.close(CloseCodes.Unknown_error);
}
}

View File

@@ -1,7 +1,7 @@
import { Payload, WebSocket } from "@fosscord/gateway";
import OPCodeHandlers from "../opcodes";
import { check } from "../opcodes/instanceOf";
import { CLOSECODES } from "../util/Constants";
import { CloseCodes } from "../util/Constants";
let erlpack: any;
try {
erlpack = require("@yukikaze-bot/erlpack");
@@ -29,13 +29,13 @@ export async function Message(this: WebSocket, buffer: Buffer) {
return;
}
if (process.env.WS_VERBOSE) console.log(`[Websocket] Incomming message: ${JSON.stringify(data)}`);
if (process.env.WS_VERBOSE) console.log(`[Gateway] Incomming message: ${JSON.stringify(data)}`);
if (data.op !== 1) check.call(this, PayloadSchema, data);
else {
//custom validation for numbers, because heartbeat
if (data.s || data.t || (typeof data.d !== "number" && data.d)) {
console.log("Invalid heartbeat...");
this.close(CLOSECODES.Decode_error);
this.close(CloseCodes.Decode_error);
}
}
@@ -44,14 +44,14 @@ export async function Message(this: WebSocket, buffer: Buffer) {
if (!OPCodeHandler) {
console.error("[Gateway] Unkown opcode " + data.op);
// TODO: if all opcodes are implemented comment this out:
// this.close(CLOSECODES.Unknown_opcode);
// this.close(CloseCodes.Unknown_opcode);
return;
}
try {
return await OPCodeHandler.call(this, data);
} catch (error) {
console.error(error);
if (!this.CLOSED && this.CLOSING) return this.close(CLOSECODES.Unknown_error);
console.error("[Gateway]", error);
if (!this.CLOSED && this.CLOSING) return this.close(CloseCodes.Unknown_error);
}
}

View File

@@ -13,7 +13,7 @@ import {
RelationshipType
} from "@fosscord/util";
import { Channel as AMQChannel } from "amqplib";
import { OPCODES } from "../util/Constants";
import { GatewayOPCodes } from "../util/Constants";
import { Send } from "../util/Send";
// TODO: close connection on Invalidated Token
@@ -27,7 +27,7 @@ export function handlePresenceUpdate(this: WebSocket, { event, acknowledge, data
acknowledge?.();
if (event === EVENTEnum.PresenceUpdate) {
return Send(this, {
op: OPCODES.Dispatch,
op: GatewayOPCodes.Dispatch,
t: event,
d: data,
s: this.sequence++
@@ -212,7 +212,7 @@ async function consume(this: WebSocket, opts: EventOpts) {
}
Send(this, {
op: OPCODES.Dispatch,
op: GatewayOPCodes.Dispatch,
t: event,
d: data,
s: this.sequence++

View File

@@ -24,7 +24,7 @@ import {
UserSettings
} from "@fosscord/util";
import { setupListener } from "../listener/listener";
import { CLOSECODES, OPCODES } from "../util/Constants";
import { CloseCodes, GatewayOPCodes } from "../util/Constants";
import { Send } from "../util/Send";
import { genSessionId } from "../util/SessionUtils";
import { check } from "./instanceOf";
@@ -46,7 +46,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
var { decoded } = await checkToken(identify.token, jwtSecret); // will throw an error if invalid
} catch (error) {
console.error("invalid token", error);
return this.close(CLOSECODES.Authentication_failed);
return this.close(CloseCodes.Authentication_failed);
}
this.user_id = decoded.id;
@@ -87,15 +87,15 @@ export async function onIdentify(this: WebSocket, data: Payload) {
Application.findOne({ where: { id: this.user_id } })
]);
if (!user) return this.close(CLOSECODES.Authentication_failed);
if (!user) return this.close(CloseCodes.Authentication_failed);
if (!user.settings) {
//settings may not exist after updating...
user.settings = new UserSettings();
user.settings.id = user.id;
//await (user.settings as UserSettings).save();
await user.settings.save();
}
if (!identify.intents) identify.intents = "30064771071";
if (!identify.intents) identify.intents = "0x6ffffffff";
this.intents = new Intents(identify.intents);
if (identify.shard) {
this.shard_id = identify.shard[0];
@@ -108,7 +108,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
this.shard_count <= 0
) {
console.log(identify.shard);
return this.close(CLOSECODES.Invalid_shard);
return this.close(CloseCodes.Invalid_shard);
}
}
let users: PublicUser[] = [];
@@ -130,7 +130,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
if (user.bot) {
setTimeout(() => {
Send(this, {
op: OPCODES.Dispatch,
op: GatewayOPCodes.Dispatch,
t: EVENTEnum.GuildCreate,
s: this.sequence++,
d: guild
@@ -223,7 +223,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
const d: ReadyEventData = {
v: 8,
application: { id: application?.id ?? "", flags: application?.flags ?? 0 }, //TODO: check this code!
application: { id: application?.id ?? "", flags: application?.flags ?? "" }, //TODO: check this code!
user: privateUser,
user_settings: user.settings,
// @ts-ignore
@@ -268,7 +268,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
// TODO: send real proper data structure
await Send(this, {
op: OPCODES.Dispatch,
op: GatewayOPCodes.Dispatch,
t: EVENTEnum.Ready,
s: this.sequence++,
d

View File

@@ -1,6 +1,6 @@
import { handlePresenceUpdate, Payload, WebSocket } from "@fosscord/gateway";
import { getOrInitialiseDatabase, getPermission, LazyRequest, listenEvent, Member, Role } from "@fosscord/util";
import { OPCODES } from "../util/Constants";
import { GatewayOPCodes } from "../util/Constants";
import { Send } from "../util/Send";
import { check } from "./instanceOf";
@@ -129,7 +129,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
});
return Send(this, {
op: OPCODES.Dispatch,
op: GatewayOPCodes.Dispatch,
s: this.sequence++,
t: "GUILD_MEMBER_LIST_UPDATE",
d: {

View File

@@ -1,5 +1,5 @@
import { Payload, WebSocket } from "@fosscord/gateway";
export function onRequestGuildMembers(this: WebSocket, data: Payload) {
// return this.close(CLOSECODES.Unknown_error);
// return this.close(CloseCodes.Unknown_error);
}

View File

@@ -8,5 +8,5 @@ export async function onResume(this: WebSocket, data: Payload) {
d: false
});
// return this.close(CLOSECODES.Invalid_session);
// return this.close(CloseCodes.Invalid_session);
}

View File

@@ -20,20 +20,16 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) {
check.call(this, VoiceStateUpdateSchema, data.d);
const body = data.d as VoiceStateUpdateSchema;
if (body.guild_id == null) {
console.log(`[Gateway] VoiceStateUpdate called with guild_id == null by user ${this.user_id}!`);
return;
}
let onlySettingsChanged = false;
let voiceState: VoiceState;
try {
voiceState = await VoiceState.findOneOrFail({
where: { user_id: this.user_id }
});
if (voiceState.session_id !== this.session_id && body.channel_id === null) {
//Should we also check guild_id === null?
//changing deaf or mute on a client that's not the one with the same session of the voicestate in the database should be ignored
return;
if (voiceState.session_id !== this.session_id) {
// new session
} else {
if (voiceState.channel_id === body.channel_id) onlySettingsChanged = true;
}
//If a user change voice channel between guild we should send a left event first
@@ -70,7 +66,7 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) {
if (voiceState.session_id !== this.session_id) voiceState.token = genVoiceToken();
voiceState.session_id = this.session_id;
const { id, ...newObj } = voiceState;
const { id, token, ...newObj } = voiceState;
await Promise.all([
voiceState.save(),
@@ -82,7 +78,7 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) {
]);
//If it's null it means that we are leaving the channel and this event is not needed
if (voiceState.channel_id !== null) {
if (voiceState.channel_id !== null && !onlySettingsChanged) {
const guild = await Guild.findOne({ where: { id: voiceState.guild_id } });
const regions = Config.get().regions;
let guildRegion: Region;
@@ -95,11 +91,11 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) {
await emitEvent({
event: "VOICE_SERVER_UPDATE",
data: {
token: voiceState.token,
token: token,
guild_id: voiceState.guild_id,
endpoint: guildRegion.endpoint
endpoint: guildRegion.endpoint ? guildRegion.endpoint + "/voice" : `localhost:${process.env.PORT || 3001}/voice`
},
guild_id: voiceState.guild_id
user_id: this.user_id
} as VoiceServerUpdateEvent);
}
}

View File

@@ -1,6 +1,6 @@
import { WebSocket } from "@fosscord/gateway";
import { instanceOf } from "@fosscord/util";
import { CLOSECODES } from "../util/Constants";
import { CloseCodes } from "../util/Constants";
export function check(this: WebSocket, schema: any, data: any) {
try {
@@ -12,7 +12,7 @@ export function check(this: WebSocket, schema: any, data: any) {
} catch (error) {
console.error(error);
// invalid payload
this.close(CLOSECODES.Decode_error);
this.close(CloseCodes.Decode_error);
throw error;
}
}

View File

@@ -1,4 +1,6 @@
export enum OPCODES {
import { VoiceOPCodes } from "@fosscord/webrtc";
export enum GatewayOPCodes {
Dispatch = 0,
Heartbeat = 1,
Identify = 2,
@@ -22,28 +24,59 @@ export enum OPCODES {
Stream_Watch = 20,
Stream_Ping = 21,
Stream_Set_Paused = 22,
Request_Application_Commands = 24
Request_Application_Commands = 24,
Embedded_Activity_Launch = 25,
Embedded_Activity_Close = 26,
Embedded_Activity_Update = 27,
Request_Forum_Unreads = 28,
Remote_Command = 29
}
export enum CLOSECODES {
export enum GatewayOPCodes {
DISPATCH = 0,
HEARTBEAT = 1,
IDENTIFY = 2,
PRESENCE_UPDATE = 3,
VOICE_STATE_UPDATE = 4,
VOICE_SERVER_PING = 5,
RESUME = 6,
RECONNECT = 7,
REQUEST_GUILD_MEMBERS = 8,
INVALID_SESSION = 9,
HELLO = 10,
HEARTBEAT_ACK = 11,
CALL_CONNECT = 13,
GUILD_SUBSCRIPTIONS = 14,
LOBBY_CONNECT = 15,
LOBBY_DISCONNECT = 16,
LOBBY_VOICE_STATES_UPDATE = 17,
STREAM_CREATE = 18,
STREAM_DELETE = 19,
STREAM_WATCH = 20,
STREAM_PING = 21,
STREAM_SET_PAUSED = 22
}
export enum CloseCodes {
Unknown_error = 4000,
Unknown_opcode,
Decode_error,
Not_authenticated,
Authentication_failed,
Already_authenticated,
Invalid_session,
Invalid_seq,
Rate_limited,
Session_timed_out,
Invalid_shard,
Sharding_required,
Invalid_API_version,
Invalid_intent,
Disallowed_intent
Unknown_opcode = 4001,
Decode_error = 4002,
Not_authenticated = 4003,
Authentication_failed = 4004,
Already_authenticated = 4005,
Invalid_session = 4006,
Invalid_seq = 4007,
Rate_limited = 4008,
Session_timed_out = 4009,
Invalid_shard = 4010,
Sharding_required = 4011,
Invalid_API_version = 4012,
Invalid_intent = 4013,
Disallowed_intent = 4014
}
export interface Payload {
op: OPCODES;
op: GatewayOPCodes | VoiceOPCodes;
d?: any;
s?: number;
t?: string;

View File

@@ -1,4 +1,4 @@
import { CLOSECODES } from "./Constants";
import { CloseCodes } from "./Constants";
import { WebSocket } from "./WebSocket";
// TODO: make heartbeat timeout configurable
@@ -6,6 +6,6 @@ export function setHeartbeat(socket: WebSocket) {
if (socket.heartbeatTimeout) clearTimeout(socket.heartbeatTimeout);
socket.heartbeatTimeout = setTimeout(() => {
return socket.close(CLOSECODES.Session_timed_out);
return socket.close(CloseCodes.Session_timed_out);
}, 1000 * 45);
}

View File

@@ -7,7 +7,7 @@ try {
import { Payload, WebSocket } from "@fosscord/gateway";
export async function Send(socket: WebSocket, data: Payload) {
if (process.env.WS_VERBOSE) console.log(`[Websocket] Outgoing message: ${JSON.stringify(data)}`);
if (process.env.WS_VERBOSE) console.log(`[Gateway] Outgoing message: ${JSON.stringify(data)}`);
let buffer: Buffer | string;
if (socket.encoding === "etf") buffer = erlpack.pack(data);
// TODO: encode circular object

View File

@@ -1,4 +1,5 @@
import { Intents, Permissions } from "@fosscord/util";
import { Client } from "@fosscord/webrtc";
import WS from "ws";
import { Deflate } from "zlib";
@@ -19,4 +20,5 @@ export interface WebSocket extends WS {
events: Record<string, Function>;
member_events: Record<string, Function>;
listen_options: any;
client: Client;
}

View File

@@ -7,7 +7,7 @@ export class RegionConfiguration {
{
id: "fosscord",
name: "Fosscord",
endpoint: "127.0.0.1:3004",
endpoint: undefined,
vip: false,
custom: false,
deprecated: false

View File

@@ -1,7 +1,9 @@
import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { URL } from "url";
import { deleteFile } from "../util/cdn";
import { BaseClass } from "./BaseClass";
import { Message } from "./Message";
@Entity("attachments")
export class Attachment extends BaseClass {
@@ -34,7 +36,7 @@ export class Attachment extends BaseClass {
@ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments, {
onDelete: "CASCADE"
})
message: import("./Message").Message;
message: Relation<Message>;
@BeforeRemove()
onDelete() {

View File

@@ -1,4 +1,5 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { ChannelPermissionOverwrite } from "./Channel";
import { User } from "./User";
@@ -97,7 +98,7 @@ export enum AuditLogEvents {
export class AuditLog extends BaseClass {
@JoinColumn({ name: "target_id" })
@ManyToOne(() => User)
target?: User;
target?: Relation<User>;
@Column({ nullable: true })
@RelationId((auditlog: AuditLog) => auditlog.user)
@@ -105,7 +106,7 @@ export class AuditLog extends BaseClass {
@JoinColumn({ name: "user_id" })
@ManyToOne(() => User, (user: User) => user.id)
user: User;
user: Relation<User>;
@Column({ type: "int" })
action_type: AuditLogEvents;

View File

@@ -6,7 +6,7 @@ import { User } from "./User";
export class BackupCode extends BaseClass {
@JoinColumn({ name: "user_id" })
@ManyToOne(() => User, { onDelete: "CASCADE" })
user: User;
user: Relation<User>;
@Column()
code: string;

View File

@@ -1,4 +1,5 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { User } from "./User";
@@ -13,7 +14,7 @@ export class Ban extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
user: User;
user: Relation<User>;
@Column({ nullable: true })
@RelationId((ban: Ban) => ban.guild)
@@ -23,7 +24,7 @@ export class Ban extends BaseClass {
@ManyToOne(() => Guild, {
onDelete: "CASCADE"
})
guild: Guild;
guild: Relation<Guild>;
@Column({ nullable: true })
@RelationId((ban: Ban) => ban.executor)
@@ -31,7 +32,7 @@ export class Ban extends BaseClass {
@JoinColumn({ name: "executor_id" })
@ManyToOne(() => User)
executor: User;
executor: Relation<User>;
@Column()
ip: string;

View File

@@ -56,7 +56,7 @@ export class Channel extends BaseClass {
cascade: true,
orphanedRowAction: "delete"
})
recipients?: Recipient[];
recipients?: Relation<Recipient[]>;
@Column({ nullable: true })
last_message_id: string;
@@ -69,7 +69,7 @@ export class Channel extends BaseClass {
@ManyToOne(() => Guild, {
onDelete: "CASCADE"
})
guild: Guild;
guild: Relation<Guild>;
@Column({ nullable: true })
@RelationId((channel: Channel) => channel.parent)
@@ -77,7 +77,7 @@ export class Channel extends BaseClass {
@JoinColumn({ name: "parent_id" })
@ManyToOne(() => Channel)
parent?: Channel;
parent?: Relation<Channel>;
// for group DMs and owned custom channel types
@Column({ nullable: true })
@@ -86,7 +86,7 @@ export class Channel extends BaseClass {
@JoinColumn({ name: "owner_id" })
@ManyToOne(() => User)
owner: User;
owner: Relation<User>;
@Column({ nullable: true })
last_pin_timestamp?: number;
@@ -112,9 +112,6 @@ export class Channel extends BaseClass {
@Column({ nullable: true })
nsfw?: boolean;
@Column({ nullable: true })
rate_limit_per_user?: number;
@Column({ nullable: true })
topic?: string;
@@ -122,7 +119,7 @@ export class Channel extends BaseClass {
cascade: true,
orphanedRowAction: "delete"
})
invites?: Invite[];
invites?: Relation<Invite[]>;
@Column({ nullable: true })
retention_policy_id?: string;
@@ -131,25 +128,25 @@ export class Channel extends BaseClass {
cascade: true,
orphanedRowAction: "delete"
})
messages?: Message[];
messages?: Relation<Message[]>;
@OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, {
cascade: true,
orphanedRowAction: "delete"
})
voice_states?: VoiceState[];
voice_states?: Relation<VoiceState[]>;
@OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, {
cascade: true,
orphanedRowAction: "delete"
})
read_states?: ReadState[];
read_states?: Relation<ReadState[]>;
@OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, {
cascade: true,
orphanedRowAction: "delete"
})
webhooks?: Webhook[];
webhooks?: Relation<Webhook[]>;
@Column({ nullable: true })
flags?: number = 0;

View File

@@ -1,4 +1,5 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { User } from "./User";
@@ -14,7 +15,7 @@ export class ConnectedAccount extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
user: User;
user: Relation<User>;
@Column({ select: false })
access_token: string;

View File

@@ -1,4 +1,5 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { User } from ".";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
@@ -18,7 +19,7 @@ export class Emoji extends BaseClass {
@ManyToOne(() => Guild, {
onDelete: "CASCADE"
})
guild: Guild;
guild: Relation<Guild>;
@Column({ nullable: true })
@RelationId((emoji: Emoji) => emoji.user)
@@ -26,7 +27,7 @@ export class Emoji extends BaseClass {
@JoinColumn({ name: "user_id" })
@ManyToOne(() => User)
user: User;
user: Relation<User>;
@Column()
managed: boolean;

View File

@@ -41,8 +41,8 @@ export class Guild extends BaseClass {
afk_channel_id?: string;
@JoinColumn({ name: "afk_channel_id" })
@ManyToOne(() => Channel)
afk_channel?: Channel;
@ManyToOne(() => Channel, { nullable: true })
afk_channel?: Relation<Channel>;
@Column({ nullable: true })
afk_timeout?: number = Config.get().defaults.guild.afkTimeout;
@@ -57,7 +57,7 @@ export class Guild extends BaseClass {
cascade: true,
orphanedRowAction: "delete"
})
bans: Ban[];
bans: Relation<Ban[]>;
@Column({ nullable: true })
banner?: string;
@@ -107,7 +107,7 @@ export class Guild extends BaseClass {
orphanedRowAction: "delete",
onDelete: "CASCADE"
})
members: Member[];
members: Relation<Member[]>;
@JoinColumn({ name: "role_ids" })
@OneToMany(() => Role, (role: Role) => role.guild, {
@@ -115,14 +115,14 @@ export class Guild extends BaseClass {
orphanedRowAction: "delete",
onDelete: "CASCADE"
})
roles: Role[];
roles: Relation<Role[]>;
@JoinColumn({ name: "channel_ids" })
@OneToMany(() => Channel, (channel: Channel) => channel.guild, {
cascade: true,
orphanedRowAction: "delete"
})
channels: Channel[];
channels: Relation<Channel[]>;
@Column({ nullable: true })
@RelationId((guild: Guild) => guild.template)
@@ -130,7 +130,7 @@ export class Guild extends BaseClass {
@JoinColumn({ name: "template_id", referencedColumnName: "id" })
@ManyToOne(() => Template)
template: Template;
template: Relation<Template>;
@JoinColumn({ name: "emoji_ids" })
@OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild, {
@@ -138,7 +138,7 @@ export class Guild extends BaseClass {
orphanedRowAction: "delete",
onDelete: "CASCADE"
})
emojis: Emoji[];
emojis: Relation<Emoji[]>;
@JoinColumn({ name: "sticker_ids" })
@OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild, {
@@ -146,7 +146,7 @@ export class Guild extends BaseClass {
orphanedRowAction: "delete",
onDelete: "CASCADE"
})
stickers: Sticker[];
stickers: Relation<Sticker[]>;
@JoinColumn({ name: "invite_ids" })
@OneToMany(() => Invite, (invite: Invite) => invite.guild, {
@@ -154,7 +154,7 @@ export class Guild extends BaseClass {
orphanedRowAction: "delete",
onDelete: "CASCADE"
})
invites: Invite[];
invites: Relation<Invite[]>;
@JoinColumn({ name: "voice_state_ids" })
@OneToMany(() => VoiceState, (voicestate: VoiceState) => voicestate.guild, {
@@ -162,7 +162,7 @@ export class Guild extends BaseClass {
orphanedRowAction: "delete",
onDelete: "CASCADE"
})
voice_states: VoiceState[];
voice_states: Relation<VoiceState[]>;
@JoinColumn({ name: "webhook_ids" })
@OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild, {
@@ -170,7 +170,7 @@ export class Guild extends BaseClass {
orphanedRowAction: "delete",
onDelete: "CASCADE"
})
webhooks: Webhook[];
webhooks: Relation<Webhook[]>;
@Column({ nullable: true })
mfa_level?: number;
@@ -183,8 +183,8 @@ export class Guild extends BaseClass {
owner_id?: string; // optional to allow for ownerless guilds
@JoinColumn({ name: "owner_id", referencedColumnName: "id" })
@ManyToOne(() => User)
owner?: User; // optional to allow for ownerless guilds
@ManyToOne(() => User, { nullable: true })
owner?: Relation<User>; // optional to allow for ownerless guilds
@Column({ nullable: true })
preferred_locale?: string;
@@ -200,16 +200,16 @@ export class Guild extends BaseClass {
public_updates_channel_id: string;
@JoinColumn({ name: "public_updates_channel_id" })
@ManyToOne(() => Channel)
public_updates_channel?: Channel;
@ManyToOne(() => Channel, { nullable: true })
public_updates_channel?: Relation<Channel>;
@Column({ nullable: true })
@RelationId((guild: Guild) => guild.rules_channel)
rules_channel_id?: string;
@JoinColumn({ name: "rules_channel_id" })
@ManyToOne(() => Channel)
rules_channel?: string;
@ManyToOne(() => Channel, { nullable: true })
rules_channel?: Relation<Channel>;
@Column({ nullable: true })
region?: string;
@@ -222,8 +222,8 @@ export class Guild extends BaseClass {
system_channel_id?: string;
@JoinColumn({ name: "system_channel_id" })
@ManyToOne(() => Channel)
system_channel?: Channel;
@ManyToOne(() => Channel, { nullable: true })
system_channel?: Relation<Channel>;
@Column({ nullable: true })
system_channel_flags?: number;
@@ -251,8 +251,8 @@ export class Guild extends BaseClass {
widget_channel_id?: string;
@JoinColumn({ name: "widget_channel_id" })
@ManyToOne(() => Channel)
widget_channel?: Channel;
@ManyToOne(() => Channel, { nullable: true })
widget_channel?: Relation<Channel>;
@Column({ nullable: true })
widget_enabled?: boolean;
@@ -333,13 +333,13 @@ export class Guild extends BaseClass {
const ids = new Map();
body.channels.forEach((x) => {
if (x.id) {
ids.set(x.id, Snowflake.generate());
body.channels!.forEach((x) => {
if (x!.id) {
ids.set(x!.id, Snowflake.generate());
}
});
for (const channel of body.channels?.sort((a, b) => (a.parent_id ? 1 : -1))) {
for (const channel of body.channels!.sort((a, b) => (a.parent_id ? 1 : -1))) {
let id = ids.get(channel.id) || Snowflake.generate();
let parent_id = ids.get(channel.parent_id);

View File

@@ -39,7 +39,7 @@ export class Invite extends BaseClassWithoutId {
@ManyToOne(() => Guild, {
onDelete: "CASCADE"
})
guild: Guild;
guild: Relation<Guild>;
@Column({ nullable: true })
@RelationId((invite: Invite) => invite.channel)
@@ -49,7 +49,7 @@ export class Invite extends BaseClassWithoutId {
@ManyToOne(() => Channel, {
onDelete: "CASCADE"
})
channel: Channel;
channel: Relation<Channel>;
@Column({ nullable: true })
@RelationId((invite: Invite) => invite.inviter)
@@ -69,7 +69,7 @@ export class Invite extends BaseClassWithoutId {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
target_user?: string; // could be used for "User specific invites" https://github.com/fosscord/fosscord/issues/62
target_user?: Relation<User>; // could be used for "User specific invites" https://github.com/fosscord/fosscord/issues/62
@Column({ nullable: true })
target_user_type?: number;

View File

@@ -40,7 +40,7 @@ export class Member extends BaseClassWithoutId {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
user: User;
user: Relation<User>;
@Column()
@RelationId((member: Member) => member.guild)
@@ -50,7 +50,7 @@ export class Member extends BaseClassWithoutId {
@ManyToOne(() => Guild, {
onDelete: "CASCADE"
})
guild: Guild;
guild: Relation<Guild>;
@Column({ nullable: true })
nick?: string;
@@ -64,7 +64,7 @@ export class Member extends BaseClassWithoutId {
}
})
@ManyToMany(() => Role, { cascade: true })
roles: Role[];
roles: Relation<Role[]>;
@Column()
joined_at: Date;
@@ -106,7 +106,7 @@ export class Member extends BaseClassWithoutId {
}
static async removeFromGuild(user_id: string, guild_id: string) {
const guild = await Guild.findOneOrFail({ select: ["owner_id", "member_count"], where: { id: guild_id } });
const guild = await require("./Guild").Guild.findOneOrFail({ select: ["owner_id", "member_count"], where: { id: guild_id } });
if (guild.owner_id === user_id) throw new Error("The owner cannot be removed of the guild");
const member = await Member.findOneOrFail({ where: { id: user_id, guild_id }, relations: ["user"] });
@@ -224,7 +224,7 @@ export class Member extends BaseClassWithoutId {
throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403);
}
const guild = await Guild.findOneOrFail({
const guild = await require("./Guild").Guild.findOneOrFail({
where: {
id: guild_id
},

View File

@@ -51,7 +51,7 @@ export class Message extends BaseClass {
@ManyToOne(() => Channel, {
onDelete: "CASCADE"
})
channel: Channel;
channel: Relation<Channel>;
@Column({ nullable: true })
@RelationId((message: Message) => message.guild)
@@ -61,7 +61,7 @@ export class Message extends BaseClass {
@ManyToOne(() => Guild, {
onDelete: "CASCADE"
})
guild?: Guild;
guild?: Relation<Guild>;
@Column({ nullable: true })
@RelationId((message: Message) => message.author)
@@ -72,7 +72,7 @@ export class Message extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
author?: User;
author?: Relation<User>;
@Column({ nullable: true })
@RelationId((message: Message) => message.member)
@@ -82,7 +82,7 @@ export class Message extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
member?: Member;
member?: Relation<Member>;
@Column({ nullable: true })
@RelationId((message: Message) => message.webhook)
@@ -90,7 +90,7 @@ export class Message extends BaseClass {
@JoinColumn({ name: "webhook_id" })
@ManyToOne(() => Webhook)
webhook?: Webhook;
webhook?: Relation<Webhook>;
@Column({ nullable: true })
@RelationId((message: Message) => message.application)
@@ -98,7 +98,7 @@ export class Message extends BaseClass {
@JoinColumn({ name: "application_id" })
@ManyToOne(() => Application)
application?: Application;
application?: Relation<Application>;
@Column({ nullable: true })
content?: string;
@@ -118,25 +118,25 @@ export class Message extends BaseClass {
@JoinTable({ name: "message_user_mentions" })
@ManyToMany(() => User)
mentions: User[];
mentions: Relation<User[]>;
@JoinTable({ name: "message_role_mentions" })
@ManyToMany(() => Role)
mention_roles: Role[];
mention_roles: Relation<Role[]>;
@JoinTable({ name: "message_channel_mentions" })
@ManyToMany(() => Channel)
mention_channels: Channel[];
mention_channels: Relation<Channel[]>;
@JoinTable({ name: "message_stickers" })
@ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" })
sticker_items?: Sticker[];
sticker_items?: Relation<Sticker[]>;
@OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, {
cascade: true,
orphanedRowAction: "delete"
})
attachments?: Attachment[];
attachments?: Relation<Attachment[]>;
@Column({ type: "simple-json" })
embeds: Embed[];
@@ -170,7 +170,7 @@ export class Message extends BaseClass {
@JoinColumn({ name: "message_reference_id" })
@ManyToOne(() => Message)
referenced_message?: Message;
referenced_message?: Relation<Message>;
@Column({ type: "simple-json", nullable: true })
interaction?: {

View File

@@ -1,3 +1,4 @@
import "reflect-metadata";
import { Column, Entity, ObjectIdColumn, PrimaryGeneratedColumn } from "typeorm";
import { BaseClassWithoutId } from ".";

View File

@@ -1,4 +1,5 @@
import { Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm";
import "reflect-metadata";
import { Column, Entity, JoinColumn, ManyToOne, Relation, Unique } from "typeorm";
import { BaseClass } from "./BaseClass";
import { User } from "./User";
@@ -7,11 +8,11 @@ import { User } from "./User";
export class Note extends BaseClass {
@JoinColumn({ name: "owner_id" })
@ManyToOne(() => User, { onDelete: "CASCADE" })
owner: User;
owner: Relation<User>;
@JoinColumn({ name: "target_id" })
@ManyToOne(() => User, { onDelete: "CASCADE" })
target: User;
target: Relation<User>;
@Column()
content: string;

View File

@@ -1,3 +1,4 @@
import "reflect-metadata";
import { Column, Entity } from "typeorm";
import { BaseClass } from "./BaseClass";

View File

@@ -1,4 +1,5 @@
import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, Index, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Channel } from "./Channel";
import { User } from "./User";
@@ -18,7 +19,7 @@ export class ReadState extends BaseClass {
@ManyToOne(() => Channel, {
onDelete: "CASCADE"
})
channel: Channel;
channel: Relation<Channel>;
@Column()
@RelationId((read_state: ReadState) => read_state.user)
@@ -28,7 +29,7 @@ export class ReadState extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
user: User;
user: Relation<User>;
// fully read marker
@Column({ nullable: true })

View File

@@ -1,5 +1,8 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Channel } from "./Channel";
import { User } from "./User";
@Entity("recipients")
export class Recipient extends BaseClass {
@@ -11,7 +14,7 @@ export class Recipient extends BaseClass {
@ManyToOne(() => require("./Channel").Channel, {
onDelete: "CASCADE"
})
channel: import("./Channel").Channel;
channel: Relation<Channel>;
@Column()
@RelationId((recipient: Recipient) => recipient.user)
@@ -21,7 +24,7 @@ export class Recipient extends BaseClass {
@ManyToOne(() => require("./User").User, {
onDelete: "CASCADE"
})
user: import("./User").User;
user: Relation<User>;
@Column({ default: false })
closed: boolean;

View File

@@ -1,4 +1,5 @@
import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, Index, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { User } from "./User";
@@ -20,7 +21,7 @@ export class Relationship extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
from: User;
from: Relation<User>;
@Column({})
@RelationId((relationship: Relationship) => relationship.to)
@@ -30,7 +31,7 @@ export class Relationship extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
to: User;
to: Relation<User>;
@Column({ nullable: true })
nickname?: string;

View File

@@ -1,4 +1,5 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
@@ -13,7 +14,7 @@ export class Role extends BaseClass {
@ManyToOne(() => Guild, {
onDelete: "CASCADE"
})
guild: Guild;
guild: Relation<Guild>;
@Column()
color: number;

View File

@@ -16,7 +16,7 @@ export class Session extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
user: User;
user: Relation<User>;
//TODO check, should be 32 char long hex string
@Column({ nullable: false, select: false })

View File

@@ -34,11 +34,11 @@ export class Sticker extends BaseClass {
pack_id?: string;
@JoinColumn({ name: "pack_id" })
@ManyToOne(() => require("./StickerPack").StickerPack, {
@ManyToOne(() => StickerPack, {
onDelete: "CASCADE",
nullable: true
})
pack: import("./StickerPack").StickerPack;
pack: Relation<StickerPack>;
@Column({ nullable: true })
guild_id?: string;
@@ -47,7 +47,7 @@ export class Sticker extends BaseClass {
@ManyToOne(() => Guild, {
onDelete: "CASCADE"
})
guild?: Guild;
guild?: Relation<Guild>;
@Column({ nullable: true })
user_id?: string;
@@ -56,7 +56,7 @@ export class Sticker extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
user?: User;
user?: Relation<User>;
@Column({ type: "int" })
type: StickerType;

View File

@@ -17,7 +17,7 @@ export class StickerPack extends BaseClass {
cascade: true,
orphanedRowAction: "delete"
})
stickers: Sticker[];
stickers: Relation<Sticker[]>;
// sku_id: string
@@ -27,5 +27,5 @@ export class StickerPack extends BaseClass {
@ManyToOne(() => Sticker, { nullable: true })
@JoinColumn()
cover_sticker?: Sticker;
cover_sticker?: Relation<Sticker>;
}

View File

@@ -12,7 +12,7 @@ export class Team extends BaseClass {
@OneToMany(() => TeamMember, (member: TeamMember) => member.team, {
orphanedRowAction: "delete"
})
members: TeamMember[];
members: Relation<TeamMember[]>;
@Column()
name: string;
@@ -23,5 +23,5 @@ export class Team extends BaseClass {
@JoinColumn({ name: "owner_user_id" })
@ManyToOne(() => User)
owner_user: User;
owner_user: Relation<User>;
}

View File

@@ -1,5 +1,7 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Team } from "./Team";
import { User } from "./User";
export enum TeamMemberState {
@@ -23,7 +25,7 @@ export class TeamMember extends BaseClass {
@ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, {
onDelete: "CASCADE"
})
team: import("./Team").Team;
team: Relation<Team>;
@Column({ nullable: true })
@RelationId((member: TeamMember) => member.user)
@@ -33,5 +35,5 @@ export class TeamMember extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
user: User;
user: Relation<User>;
}

View File

@@ -1,4 +1,5 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { User } from "./User";
@@ -23,7 +24,7 @@ export class Template extends BaseClass {
@JoinColumn({ name: "creator_id" })
@ManyToOne(() => User)
creator: User;
creator: Relation<User>;
@Column()
created_at: Date;
@@ -37,7 +38,7 @@ export class Template extends BaseClass {
@JoinColumn({ name: "source_guild_id" })
@ManyToOne(() => Guild)
source_guild: Guild;
source_guild: Relation<Guild>;
@Column({ type: "simple-json" })
serialized_source_guild: Guild;

View File

@@ -141,21 +141,21 @@ export class User extends BaseClass {
rights: string = Config.get().register.defaultRights; // Rights
@OneToMany(() => Session, (session: Session) => session.user)
sessions: Session[];
sessions: Relation<Session[]>;
@JoinColumn({ name: "relationship_ids" })
@OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, {
cascade: true,
orphanedRowAction: "delete"
})
relationships: Relationship[];
relationships: Relation<Relationship[]>;
@JoinColumn({ name: "connected_account_ids" })
@OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user, {
cascade: true,
orphanedRowAction: "delete"
})
connected_accounts: ConnectedAccount[];
connected_accounts: Relation<ConnectedAccount[]>;
@Column({ type: "simple-json", select: false })
data: {
@@ -264,6 +264,8 @@ export class User extends BaseClass {
// appearently discord doesn't save the date of birth and just calculate if nsfw is allowed
// if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false
const language = req?.language === "en" ? "en-US" : req?.language || "en-US";
const settings = new UserSettings();
settings.locale = language;
const user = OrmUtils.mergeDeep(new User(), {
//required:
@@ -280,11 +282,14 @@ export class User extends BaseClass {
//await (user.settings as UserSettings).save();
await user.save();
await user.settings.save();
setImmediate(async () => {
if (Config.get().guild.autoJoin.enabled) {
for (const guild of Config.get().guild.autoJoin.guilds || []) {
await Member.addToGuild(user.id, guild).catch((e) => {});
await require("./Member")
.Member.addToGuild(user.id, guild)
.catch((e: any) => {});
}
}
});

View File

@@ -1,4 +1,5 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Channel } from "./Channel";
import { Guild } from "./Guild";
@@ -16,7 +17,7 @@ export class VoiceState extends BaseClass {
@ManyToOne(() => Guild, {
onDelete: "CASCADE"
})
guild?: Guild;
guild?: Relation<Guild>;
@Column({ nullable: true })
@RelationId((voice_state: VoiceState) => voice_state.channel)
@@ -26,7 +27,7 @@ export class VoiceState extends BaseClass {
@ManyToOne(() => Channel, {
onDelete: "CASCADE"
})
channel: Channel;
channel: Relation<Channel>;
@Column({ nullable: true })
@RelationId((voice_state: VoiceState) => voice_state.user)
@@ -36,20 +37,20 @@ export class VoiceState extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
user: User;
user: Relation<User>;
// @JoinColumn([{ name: "user_id", referencedColumnName: "id" },{ name: "guild_id", referencedColumnName: "guild_id" }])
// @ManyToOne(() => Member, {
// onDelete: "CASCADE",
// })
//TODO find a way to make it work without breaking Guild.voice_states
member: Member;
member: Relation<Member>;
@Column()
session_id: string;
@Column({ nullable: true })
token: string;
token?: string;
@Column()
deaf: boolean;

View File

@@ -1,4 +1,5 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import "reflect-metadata";
import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm";
import { Application } from "./Application";
import { BaseClass } from "./BaseClass";
import { Channel } from "./Channel";
@@ -32,7 +33,7 @@ export class Webhook extends BaseClass {
@ManyToOne(() => Guild, {
onDelete: "CASCADE"
})
guild: Guild;
guild: Relation<Guild>;
@Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.channel)
@@ -42,7 +43,7 @@ export class Webhook extends BaseClass {
@ManyToOne(() => Channel, {
onDelete: "CASCADE"
})
channel: Channel;
channel: Relation<Channel>;
@Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.application)
@@ -52,7 +53,7 @@ export class Webhook extends BaseClass {
@ManyToOne(() => Application, {
onDelete: "CASCADE"
})
application: Application;
application: Relation<Application>;
@Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.user)
@@ -62,7 +63,7 @@ export class Webhook extends BaseClass {
@ManyToOne(() => User, {
onDelete: "CASCADE"
})
user: User;
user: Relation<User>;
@Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.guild)
@@ -72,5 +73,5 @@ export class Webhook extends BaseClass {
@ManyToOne(() => Guild, {
onDelete: "CASCADE"
})
source_guild: Guild;
source_guild: Relation<Guild>;
}

View File

@@ -0,0 +1,231 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CodeCleanup31660258393551 implements MigrationInterface {
name = "CodeCleanup31660258393551";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE \`applications\` DROP FOREIGN KEY \`FK_2ce5a55796fe4c2f77ece57a647\`
`);
await queryRunner.query(`
DROP INDEX \`REL_2ce5a55796fe4c2f77ece57a64\` ON \`applications\`
`);
await queryRunner.query(`
CREATE TABLE \`user_settings\` (
\`id\` varchar(255) NOT NULL,
\`afk_timeout\` int NULL,
\`allow_accessibility_detection\` tinyint NULL,
\`animate_emoji\` tinyint NULL,
\`animate_stickers\` int NULL,
\`contact_sync_enabled\` tinyint NULL,
\`convert_emoticons\` tinyint NULL,
\`custom_status\` text NULL,
\`default_guilds_restricted\` tinyint NULL,
\`detect_platform_accounts\` tinyint NULL,
\`developer_mode\` tinyint NULL,
\`disable_games_tab\` tinyint NULL,
\`enable_tts_command\` tinyint NULL,
\`explicit_content_filter\` int NULL,
\`friend_source_flags\` text NULL,
\`gateway_connected\` tinyint NULL,
\`gif_auto_play\` tinyint NULL,
\`guild_folders\` text NULL,
\`guild_positions\` text NULL,
\`inline_attachment_media\` tinyint NULL,
\`inline_embed_media\` tinyint NULL,
\`locale\` varchar(255) NULL,
\`message_display_compact\` tinyint NULL,
\`native_phone_integration_enabled\` tinyint NULL,
\`render_embeds\` tinyint NULL,
\`render_reactions\` tinyint NULL,
\`restricted_guilds\` text NULL,
\`show_current_game\` tinyint NULL,
\`status\` varchar(255) NULL,
\`stream_notifications_enabled\` tinyint NULL,
\`theme\` varchar(255) NULL,
\`timezone_offset\` int NULL,
PRIMARY KEY (\`id\`)
) ENGINE = InnoDB
`);
await queryRunner.query(`
ALTER TABLE \`users\` DROP COLUMN \`settings\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`type\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`hook\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`redirect_uris\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`rpc_application_state\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`store_application_state\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`verification_state\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`interactions_endpoint_url\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`integration_public\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`integration_require_code_grant\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`discoverability_state\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`discovery_eligibility_flags\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`tags\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`install_params\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`bot_user_id\`
`);
await queryRunner.query(`
ALTER TABLE \`guilds\`
ADD \`premium_progress_bar_enabled\` tinyint NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`rpc_origins\` text NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`primary_sku_id\` varchar(255) NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`slug\` varchar(255) NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`guild_id\` varchar(255) NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\` CHANGE \`description\` \`description\` varchar(255) NOT NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`flags\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`flags\` varchar(255) NOT NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD CONSTRAINT \`FK_e5bf78cdbbe9ba91062d74c5aba\` FOREIGN KEY (\`guild_id\`) REFERENCES \`guilds\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE \`applications\` DROP FOREIGN KEY \`FK_e5bf78cdbbe9ba91062d74c5aba\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`flags\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`flags\` int NOT NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\` CHANGE \`description\` \`description\` varchar(255) NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`guild_id\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`slug\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`primary_sku_id\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\` DROP COLUMN \`rpc_origins\`
`);
await queryRunner.query(`
ALTER TABLE \`guilds\` DROP COLUMN \`premium_progress_bar_enabled\`
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`bot_user_id\` varchar(255) NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`install_params\` text NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`tags\` text NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`discovery_eligibility_flags\` int NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`discoverability_state\` int NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`integration_require_code_grant\` tinyint NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`integration_public\` tinyint NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`interactions_endpoint_url\` varchar(255) NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`verification_state\` int NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`store_application_state\` int NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`rpc_application_state\` int NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`redirect_uris\` text NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`hook\` tinyint NOT NULL
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD \`type\` text NULL
`);
await queryRunner.query(`
ALTER TABLE \`users\`
ADD \`settings\` text NOT NULL
`);
await queryRunner.query(`
DROP TABLE \`user_settings\`
`);
await queryRunner.query(`
CREATE UNIQUE INDEX \`REL_2ce5a55796fe4c2f77ece57a64\` ON \`applications\` (\`bot_user_id\`)
`);
await queryRunner.query(`
ALTER TABLE \`applications\`
ADD CONSTRAINT \`FK_2ce5a55796fe4c2f77ece57a647\` FOREIGN KEY (\`bot_user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION
`);
}
}

View File

@@ -0,0 +1,38 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CodeCleanup41660260587556 implements MigrationInterface {
name = "CodeCleanup41660260587556";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE \`users\`
ADD \`settingsId\` varchar(255) NULL
`);
await queryRunner.query(`
ALTER TABLE \`users\`
ADD UNIQUE INDEX \`IDX_76ba283779c8441fd5ff819c8c\` (\`settingsId\`)
`);
await queryRunner.query(`
CREATE UNIQUE INDEX \`REL_76ba283779c8441fd5ff819c8c\` ON \`users\` (\`settingsId\`)
`);
await queryRunner.query(`
ALTER TABLE \`users\`
ADD CONSTRAINT \`FK_76ba283779c8441fd5ff819c8cf\` FOREIGN KEY (\`settingsId\`) REFERENCES \`user_settings\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE \`users\` DROP FOREIGN KEY \`FK_76ba283779c8441fd5ff819c8cf\`
`);
await queryRunner.query(`
DROP INDEX \`REL_76ba283779c8441fd5ff819c8c\` ON \`users\`
`);
await queryRunner.query(`
ALTER TABLE \`users\` DROP INDEX \`IDX_76ba283779c8441fd5ff819c8c\`
`);
await queryRunner.query(`
ALTER TABLE \`users\` DROP COLUMN \`settingsId\`
`);
}
}

View File

@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CodeCleanup51660265930624 implements MigrationInterface {
name = "CodeCleanup51660265930624";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE \`users\`
ADD \`settingsId\` varchar(255) NULL
`);
await queryRunner.query(`
ALTER TABLE \`users\`
ADD UNIQUE INDEX \`IDX_76ba283779c8441fd5ff819c8c\` (\`settingsId\`)
`);
await queryRunner.query(`
ALTER TABLE \`channels\`
ADD \`flags\` int NULL
`);
await queryRunner.query(`
ALTER TABLE \`channels\`
ADD \`default_thread_rate_limit_per_user\` int NULL
`);
await queryRunner.query(`
CREATE UNIQUE INDEX \`REL_76ba283779c8441fd5ff819c8c\` ON \`users\` (\`settingsId\`)
`);
await queryRunner.query(`
ALTER TABLE \`users\`
ADD CONSTRAINT \`FK_76ba283779c8441fd5ff819c8cf\` FOREIGN KEY (\`settingsId\`) REFERENCES \`user_settings\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE \`users\` DROP FOREIGN KEY \`FK_76ba283779c8441fd5ff819c8cf\`
`);
await queryRunner.query(`
DROP INDEX \`REL_76ba283779c8441fd5ff819c8c\` ON \`users\`
`);
await queryRunner.query(`
ALTER TABLE \`channels\` DROP COLUMN \`default_thread_rate_limit_per_user\`
`);
await queryRunner.query(`
ALTER TABLE \`channels\` DROP COLUMN \`flags\`
`);
await queryRunner.query(`
ALTER TABLE \`users\` DROP INDEX \`IDX_76ba283779c8441fd5ff819c8c\`
`);
await queryRunner.query(`
ALTER TABLE \`users\` DROP COLUMN \`settingsId\`
`);
}
}

13
src/util/plugin/Plugin.ts Normal file
View File

@@ -0,0 +1,13 @@
import { TypedEventEmitter } from "@fosscord/util";
import EventEmitter from "events";
type PluginEvents = {
error: (error: Error | unknown) => void;
loaded: () => void;
};
export class Plugin extends (EventEmitter as new () => TypedEventEmitter<PluginEvents>) {
async init() {
// insert default config into database?
}
}

View File

@@ -0,0 +1,39 @@
import fs from "fs";
import path from "path";
import { Plugin, PluginManifest } from "./";
const root = process.env.PLUGIN_LOCATION || "../plugins";
let pluginsLoaded = false;
export class PluginLoader {
public static loadPlugins() {
console.log(`Plugin root directory: ${path.resolve(root)}`);
const dirs = fs.readdirSync(root).filter((x) => {
try {
fs.readdirSync(path.join(root, x));
return true;
} catch (e) {
return false;
}
});
console.log(dirs);
dirs.forEach(async (x) => {
let modPath = path.resolve(path.join(root, x));
console.log(`Trying to load plugin: ${modPath}`);
const manifest = require(path.join(modPath, "plugin.json")) as PluginManifest;
console.log(
`Plugin info: ${manifest.name} (${manifest.id}), written by ${manifest.authors}, available at ${manifest.repository}`
);
const module_ = require(path.join(modPath, "dist", "index.js")) as Plugin;
try {
await module_.init();
module_.emit("loaded");
} catch (error) {
module_.emit("error", error);
}
});
//
//module_.pluginPath =
}
}

View File

@@ -0,0 +1,9 @@
export class PluginManifest {
id: string;
name: string;
authors: string[];
repository: string;
license: string;
version: string; // semver
versionCode: number; // integer
}

3
src/util/plugin/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from "./Plugin";
export * from "./PluginLoader";
export * from "./PluginManifest";

View File

@@ -0,0 +1,19 @@
export interface SelectProtocolSchema {
protocol: "webrtc" | "udp";
data:
| string
| {
address: string;
port: number;
mode: string;
};
sdp?: string;
codecs?: {
name: "opus" | "VP8" | "VP9" | "H264";
type: "audio" | "video";
priority: number;
payload_type: number;
rtx_payload_type?: number | null;
}[];
rtc_connection_id?: string; // uuid
}

View File

@@ -0,0 +1,54 @@
import Ajv from "ajv";
import addFormats from "ajv-formats";
import fs from "fs";
import path from "path";
const SchemaPath = path.join(__dirname, "..", "..", "..", "assets", "schemas.json");
const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
export const ajv = new Ajv({
allErrors: true,
parseDate: true,
allowDate: true,
schemas,
coerceTypes: true,
messages: true,
strict: true,
strictRequired: true
});
addFormats(ajv);
export function validateSchema<G>(schema: string, data: G): G {
const valid = ajv.validate(schema, normalizeBody(data));
if (!valid) throw ajv.errors;
return data;
}
// Normalizer is introduced to workaround https://github.com/ajv-validator/ajv/issues/1287
// this removes null values as ajv doesn't treat them as undefined
// normalizeBody allows to handle circular structures without issues
// taken from https://github.com/serverless/serverless/blob/master/lib/classes/ConfigSchemaHandler/index.js#L30 (MIT license)
export const normalizeBody = (body: any = {}) => {
const normalizedObjectsSet = new WeakSet();
const normalizeObject = (object: any) => {
if (normalizedObjectsSet.has(object)) return;
normalizedObjectsSet.add(object);
if (Array.isArray(object)) {
for (const [index, value] of object.entries()) {
if (typeof value === "object") normalizeObject(value);
}
} else {
for (const [key, value] of Object.entries(object)) {
if (value == null) {
if (key === "icon" || key === "avatar" || key === "banner" || key === "splash" || key === "discovery_splash") continue;
delete object[key];
} else if (typeof value === "object") {
normalizeObject(value);
}
}
}
};
normalizeObject(body);
return body;
};

View File

@@ -0,0 +1,12 @@
export interface VoiceIdentifySchema {
server_id: string;
user_id: string;
session_id: string;
token: string;
video?: boolean;
streams?: {
type: string;
rid: string;
quality: number;
}[];
}

View File

@@ -0,0 +1,17 @@
export interface VoiceVideoSchema {
audio_ssrc: number;
video_ssrc: number;
rtx_ssrc?: number;
user_id?: string;
streams?: {
type: "video" | "audio";
rid: string;
ssrc: number;
active: boolean;
quality: number;
rtx_ssrc: number;
max_bitrate: number;
max_framerate: number;
max_resolution: { type: string; width: number; height: number };
}[];
}

View File

@@ -30,6 +30,7 @@ export * from "./RelationshipPostSchema";
export * from "./RelationshipPutSchema";
export * from "./RoleModifySchema";
export * from "./RolePositionUpdateSchema";
export * from "./SelectProtocolSchema";
export * from "./TemplateCreateSchema";
export * from "./TemplateModifySchema";
export * from "./TotpDisableSchema";
@@ -37,7 +38,10 @@ export * from "./TotpEnableSchema";
export * from "./TotpSchema";
export * from "./UserModifySchema";
export * from "./UserSettingsSchema";
export * from "./Validator";
export * from "./VanityUrlSchema";
export * from "./VoiceIdentifySchema";
export * from "./VoiceStateUpdateSchema";
export * from "./VoiceVideoSchema";
export * from "./WebhookCreateSchema";
export * from "./WidgetModifySchema";

View File

@@ -91,6 +91,7 @@ function getDataSourceOptions(): DataSourceOptions {
cache: {
duration: 1000 * 3 // cache all find queries for 3 seconds
},
// relationLoadStrategy: "query",
bigNumberStrings: false,
supportBigNumbers: true,
name: "default",

View File

@@ -0,0 +1,12 @@
import { DefaultNamingStrategy } from "typeorm";
export class NamingStrategy extends DefaultNamingStrategy {
eagerJoinRelationAlias(alias: string, propertyPath: string) {
const result = super.eagerJoinRelationAlias(alias, propertyPath);
console.log({ alias, propertyPath, result });
return result;
}
}
export const namingStrategy = new NamingStrategy();

View File

@@ -0,0 +1,41 @@
export type EventMap = {
[key: string]: (...args: any[]) => void;
};
/**
* Type-safe event emitter.
*
* Use it like this:
*
* ```typescript
* type MyEvents = {
* error: (error: Error) => void;
* message: (from: string, content: string) => void;
* }
*
* const myEmitter = new EventEmitter() as TypedEmitter<MyEvents>;
*
* myEmitter.emit("error", "x") // <- Will catch this type error;
* ```
*/
export interface TypedEventEmitter<Events extends EventMap> {
addListener<E extends keyof Events>(event: E, listener: Events[E]): this;
on<E extends keyof Events>(event: E, listener: Events[E]): this;
once<E extends keyof Events>(event: E, listener: Events[E]): this;
prependListener<E extends keyof Events>(event: E, listener: Events[E]): this;
prependOnceListener<E extends keyof Events>(event: E, listener: Events[E]): this;
off<E extends keyof Events>(event: E, listener: Events[E]): this;
removeAllListeners<E extends keyof Events>(event?: E): this;
removeListener<E extends keyof Events>(event: E, listener: Events[E]): this;
emit<E extends keyof Events>(event: E, ...args: Parameters<Events[E]>): boolean;
// The sloppy `eventNames()` return type is to mitigate type incompatibilities - see #5
eventNames(): (keyof Events | string | symbol)[];
rawListeners<E extends keyof Events>(event: E): Events[E][];
listeners<E extends keyof Events>(event: E): Events[E][];
listenerCount<E extends keyof Events>(event: E): number;
getMaxListeners(): number;
setMaxListeners(maxListeners: number): this;
}

56
src/webrtc/Server.ts Normal file
View File

@@ -0,0 +1,56 @@
import { closeDatabase, Config, getOrInitialiseDatabase, initEvent } from "@fosscord/util";
import dotenv from "dotenv";
import http from "http";
import ws from "ws";
import { Connection } from "./events/Connection";
dotenv.config();
export class Server {
public ws: ws.Server;
public port: number;
public server: http.Server;
public production: boolean;
constructor({ port, server, production }: { port: number; server?: http.Server; production?: boolean }) {
this.port = port;
this.production = production || false;
if (server) this.server = server;
else {
this.server = http.createServer(function (req, res) {
res.writeHead(200).end("Online");
});
}
this.server.on("upgrade", (request, socket, head) => {
if (!request.url?.includes("voice")) return;
this.ws.handleUpgrade(request, socket, head, (socket) => {
// @ts-ignore
socket.server = this;
this.ws.emit("connection", socket, request);
});
});
this.ws = new ws.Server({
maxPayload: 1024 * 1024 * 100,
noServer: true
});
this.ws.on("connection", Connection);
this.ws.on("error", console.error);
}
async start(): Promise<void> {
await getOrInitialiseDatabase();
await Config.init();
await initEvent();
if (!this.server.listening) {
this.server.listen(this.port);
console.log(`[WebRTC] online on 0.0.0.0:${this.port}`);
}
}
async stop() {
closeDatabase();
this.server.close();
}
}

View File

@@ -0,0 +1,9 @@
import { WebSocket } from "@fosscord/gateway";
import { Session } from "@fosscord/util";
export async function onClose(this: WebSocket, code: number, reason: string) {
console.log("[WebRTC] closed", code, reason.toString());
if (this.session_id) await Session.delete({ session_id: this.session_id });
this.removeAllListeners();
}

View File

@@ -0,0 +1,60 @@
import { CloseCodes, Send, setHeartbeat, WebSocket } from "@fosscord/gateway";
import { IncomingMessage } from "http";
import { URL } from "url";
import WS from "ws";
import { VoiceOPCodes } from "../util";
import { onClose } from "./Close";
import { onMessage } from "./Message";
var erlpack: any;
try {
erlpack = require("@yukikaze-bot/erlpack");
} catch (error) {}
// TODO: check rate limit
// TODO: specify rate limit in config
// TODO: check msg max size
export async function Connection(this: WS.Server, socket: WebSocket, request: IncomingMessage) {
try {
socket.on("close", onClose.bind(socket));
socket.on("message", onMessage.bind(socket));
console.log("[WebRTC] new connection", request.url);
if (process.env.WS_LOGEVENTS) {
[
"close",
"error",
"upgrade",
//"message",
"open",
"ping",
"pong",
"unexpected-response"
].forEach((x) => {
socket.on(x, (y) => console.log("[WebRTC]", x, y));
});
}
const { searchParams } = new URL(`http://localhost${request.url}`);
socket.encoding = "json";
socket.version = Number(searchParams.get("v")) || 5;
if (socket.version < 3) return socket.close(CloseCodes.Unknown_error, "invalid version");
setHeartbeat(socket);
socket.readyTimeout = setTimeout(() => {
return socket.close(CloseCodes.Session_timed_out);
}, 1000 * 30);
await Send(socket, {
op: VoiceOPCodes.HELLO,
d: {
heartbeat_interval: 1000 * 30
}
});
} catch (error) {
console.error("[WebRTC]", error);
return socket.close(CloseCodes.Unknown_error);
}
}

View File

@@ -0,0 +1,38 @@
import { CloseCodes, Payload, WebSocket } from "@fosscord/gateway";
import { Tuple } from "lambert-server";
import OPCodeHandlers from "../opcodes";
import { VoiceOPCodes } from "../util";
const PayloadSchema = {
op: Number,
$d: new Tuple(Object, Number), // or number for heartbeat sequence
$s: Number,
$t: String
};
export async function onMessage(this: WebSocket, buffer: Buffer) {
try {
var data: Payload = JSON.parse(buffer.toString());
if (data.op !== VoiceOPCodes.IDENTIFY && !this.user_id) return this.close(CloseCodes.Not_authenticated);
// @ts-ignore
const OPCodeHandler = OPCodeHandlers[data.op];
if (!OPCodeHandler) {
// @ts-ignore
console.error("[WebRTC] Unkown opcode " + VoiceOPCodes[data.op]);
// TODO: if all opcodes are implemented comment this out:
// this.close(CloseCodes.Unknown_opcode);
return;
}
if (![VoiceOPCodes.HEARTBEAT, VoiceOPCodes.SPEAKING].includes(data.op as VoiceOPCodes)) {
// @ts-ignore
console.log("[WebRTC] Opcode " + VoiceOPCodes[data.op]);
}
return await OPCodeHandler.call(this, data);
} catch (error) {
console.error("[WebRTC] error", error);
// if (!this.CLOSED && this.CLOSING) return this.close(CloseCodes.Unknown_error);
}
}

2
src/webrtc/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./Server";
export * from "./util/index";

View File

@@ -0,0 +1,6 @@
import { Payload, Send, WebSocket } from "@fosscord/gateway";
import { VoiceOPCodes } from "../util";
export async function onBackendVersion(this: WebSocket, data: Payload) {
await Send(this, { op: VoiceOPCodes.VOICE_BACKEND_VERSION, d: { voice: "0.8.43", rtc_worker: "0.3.26" } });
}

View File

@@ -0,0 +1,9 @@
import { CloseCodes, Payload, Send, setHeartbeat, WebSocket } from "@fosscord/gateway";
import { VoiceOPCodes } from "../util";
export async function onHeartbeat(this: WebSocket, data: Payload) {
setHeartbeat(this);
if (isNaN(data.d)) return this.close(CloseCodes.Decode_error);
await Send(this, { op: VoiceOPCodes.HEARTBEAT_ACK, d: data.d });
}

View File

@@ -0,0 +1,60 @@
import { CloseCodes, Payload, Send, WebSocket } from "@fosscord/gateway";
import { validateSchema, VoiceIdentifySchema, VoiceState } from "@fosscord/util";
import { endpoint, getClients, VoiceOPCodes } from "@fosscord/webrtc";
import SemanticSDP from "semantic-sdp";
const defaultSDP = require("../../../assets/sdp.json");
export async function onIdentify(this: WebSocket, data: Payload) {
clearTimeout(this.readyTimeout);
const { server_id, user_id, session_id, token, streams, video } = validateSchema("VoiceIdentifySchema", data.d) as VoiceIdentifySchema;
const voiceState = await VoiceState.findOneBy({ guild_id: server_id, user_id, token, session_id });
if (!voiceState) return this.close(CloseCodes.Authentication_failed);
this.user_id = user_id;
this.session_id = session_id;
const sdp = SemanticSDP.SDPInfo.expand(defaultSDP);
sdp.setDTLS(SemanticSDP.DTLSInfo.expand({ setup: "actpass", hash: "sha-256", fingerprint: endpoint.getDTLSFingerprint() }));
this.client = {
websocket: this,
out: {
tracks: new Map()
},
in: {
audio_ssrc: 0,
video_ssrc: 0,
rtx_ssrc: 0
},
sdp,
channel_id: voiceState.channel_id
};
const clients = getClients(voiceState.channel_id)!;
clients.add(this.client);
this.on("close", () => {
clients.delete(this.client);
});
await Send(this, {
op: VoiceOPCodes.READY,
d: {
streams: [
// { type: "video", ssrc: this.ssrc + 1, rtx_ssrc: this.ssrc + 2, rid: "100", quality: 100, active: false }
],
ssrc: -1,
port: endpoint.getLocalPort(),
modes: [
"aead_aes256_gcm_rtpsize",
"aead_aes256_gcm",
"xsalsa20_poly1305_lite_rtpsize",
"xsalsa20_poly1305_lite",
"xsalsa20_poly1305_suffix",
"xsalsa20_poly1305"
],
ip: "127.0.0.1",
experiments: []
}
});
}

View File

@@ -0,0 +1,44 @@
import { Payload, Send, WebSocket } from "@fosscord/gateway";
import { SelectProtocolSchema, validateSchema } from "@fosscord/util";
import { endpoint, PublicIP, VoiceOPCodes } from "@fosscord/webrtc";
import SemanticSDP from "semantic-sdp";
export async function onSelectProtocol(this: WebSocket, payload: Payload) {
const data = validateSchema("SelectProtocolSchema", payload.d) as SelectProtocolSchema;
const offer = SemanticSDP.SDPInfo.parse("m=audio\n" + data.sdp!);
this.client.sdp!.setICE(offer.getICE());
this.client.sdp!.setDTLS(offer.getDTLS());
const transport = endpoint.createTransport(this.client.sdp!);
this.client.transport = transport;
transport.setRemoteProperties(this.client.sdp!);
transport.setLocalProperties(this.client.sdp!);
const dtls = transport.getLocalDTLSInfo();
const ice = transport.getLocalICEInfo();
const port = endpoint.getLocalPort();
const fingerprint = dtls.getHash() + " " + dtls.getFingerprint();
const candidates = transport.getLocalCandidates();
const candidate = candidates[0];
const answer = `m=audio ${port} ICE/SDP
a=fingerprint:${fingerprint}
c=IN IP4 ${PublicIP}
a=rtcp:${port}
a=ice-ufrag:${ice.getUfrag()}
a=ice-pwd:${ice.getPwd()}
a=fingerprint:${fingerprint}
a=candidate:1 1 ${candidate.getTransport()} ${candidate.getFoundation()} ${candidate.getAddress()} ${candidate.getPort()} typ host
`;
await Send(this, {
op: VoiceOPCodes.SELECT_PROTOCOL_ACK,
d: {
video_codec: "H264",
sdp: answer,
media_session_id: this.session_id,
audio_codec: "opus"
}
});
}

View File

@@ -0,0 +1,22 @@
import { Payload, Send, WebSocket } from "@fosscord/gateway";
import { getClients, VoiceOPCodes } from "../util";
// {"speaking":1,"delay":5,"ssrc":2805246727}
export async function onSpeaking(this: WebSocket, data: Payload) {
if (!this.client) return;
getClients(this.client.channel_id).forEach((client) => {
if (client === this.client) return;
const ssrc = this.client.out.tracks.get(client.websocket.user_id);
Send(client.websocket, {
op: VoiceOPCodes.SPEAKING,
d: {
user_id: client.websocket.user_id,
speaking: data.d.speaking,
ssrc: ssrc?.audio_ssrc || 0
}
});
});
}

117
src/webrtc/opcodes/Video.ts Normal file
View File

@@ -0,0 +1,117 @@
import { Payload, Send, WebSocket } from "@fosscord/gateway";
import { validateSchema, VoiceVideoSchema } from "@fosscord/util";
import { channels, getClients, VoiceOPCodes } from "@fosscord/webrtc";
import { IncomingStreamTrack, SSRCs } from "medooze-media-server";
import SemanticSDP from "semantic-sdp";
export async function onVideo(this: WebSocket, payload: Payload) {
if (!this.client) return;
const { transport, channel_id } = this.client;
if (!transport) return;
const d = validateSchema("VoiceVideoSchema", payload.d) as VoiceVideoSchema;
await Send(this, { op: VoiceOPCodes.MEDIA_SINK_WANTS, d: { any: 100 } });
const id = "stream" + this.user_id;
var stream = this.client.in.stream!;
if (!stream) {
stream = this.client.transport!.createIncomingStream(
// @ts-ignore
SemanticSDP.StreamInfo.expand({
id,
// @ts-ignore
tracks: []
})
);
this.client.in.stream = stream;
const interval = setInterval(() => {
for (const track of stream.getTracks()) {
for (const layer of Object.values(track.getStats())) {
console.log(track.getId(), layer.total);
}
}
}, 5000);
stream.on("stopped", () => {
console.log("stream stopped");
clearInterval(interval);
});
this.on("close", () => {
transport!.stop();
});
const out = transport.createOutgoingStream(
// @ts-ignore
SemanticSDP.StreamInfo.expand({
id: "out" + this.user_id,
// @ts-ignore
tracks: []
})
);
this.client.out.stream = out;
const clients = channels.get(channel_id)!;
clients.forEach((client) => {
if (client.websocket.user_id === this.user_id) return;
if (!client.in.stream) return;
client.in.stream?.getTracks().forEach((track) => {
attachTrack.call(this, track, client.websocket.user_id);
});
});
}
if (d.audio_ssrc) {
handleSSRC.call(this, "audio", { media: d.audio_ssrc, rtx: d.audio_ssrc + 1 });
}
if (d.video_ssrc && d.rtx_ssrc) {
handleSSRC.call(this, "video", { media: d.video_ssrc, rtx: d.rtx_ssrc });
}
}
function attachTrack(this: WebSocket, track: IncomingStreamTrack, user_id: string) {
if (!this.client) return;
const outTrack = this.client.transport!.createOutgoingStreamTrack(track.getMedia());
outTrack.attachTo(track);
this.client.out.stream!.addTrack(outTrack);
var ssrcs = this.client.out.tracks.get(user_id)!;
if (!ssrcs) ssrcs = this.client.out.tracks.set(user_id, { audio_ssrc: 0, rtx_ssrc: 0, video_ssrc: 0 }).get(user_id)!;
if (track.getMedia() === "audio") {
ssrcs.audio_ssrc = outTrack.getSSRCs().media!;
} else if (track.getMedia() === "video") {
ssrcs.video_ssrc = outTrack.getSSRCs().media!;
ssrcs.rtx_ssrc = outTrack.getSSRCs().rtx!;
}
Send(this, {
op: VoiceOPCodes.VIDEO,
d: {
user_id: user_id,
...ssrcs
} as VoiceVideoSchema
});
}
function handleSSRC(this: WebSocket, type: "audio" | "video", ssrcs: SSRCs) {
const stream = this.client.in.stream!;
const transport = this.client.transport!;
const id = type + ssrcs.media;
var track = stream.getTrack(id);
if (!track) {
console.log("createIncomingStreamTrack", id);
track = transport.createIncomingStreamTrack(type, { id, ssrcs });
stream.addTrack(track);
const clients = getClients(this.client.channel_id)!;
clients.forEach((client) => {
if (client.websocket.user_id === this.user_id) return;
if (!client.out.stream) return;
attachTrack.call(this, track, client.websocket.user_id);
});
}
}

View File

@@ -0,0 +1,19 @@
import { Payload, WebSocket } from "@fosscord/gateway";
import { VoiceOPCodes } from "../util";
import { onBackendVersion } from "./BackendVersion";
import { onHeartbeat } from "./Heartbeat";
import { onIdentify } from "./Identify";
import { onSelectProtocol } from "./SelectProtocol";
import { onSpeaking } from "./Speaking";
import { onVideo } from "./Video";
export type OPCodeHandler = (this: WebSocket, data: Payload) => any;
export default {
[VoiceOPCodes.HEARTBEAT]: onHeartbeat,
[VoiceOPCodes.IDENTIFY]: onIdentify,
[VoiceOPCodes.VOICE_BACKEND_VERSION]: onBackendVersion,
[VoiceOPCodes.VIDEO]: onVideo,
[VoiceOPCodes.SPEAKING]: onSpeaking,
[VoiceOPCodes.SELECT_PROTOCOL]: onSelectProtocol
};

13
src/webrtc/start.ts Normal file
View File

@@ -0,0 +1,13 @@
process.on("uncaughtException", console.error);
process.on("unhandledRejection", console.error);
import { config } from "dotenv";
import { Server } from "./Server";
config();
const port = Number(process.env.PORT) || 3004;
const server = new Server({
port
});
server.start();

View File

@@ -0,0 +1,26 @@
export enum VoiceStatus {
CONNECTED = 0,
CONNECTING = 1,
AUTHENTICATING = 2,
RECONNECTING = 3,
DISCONNECTED = 4
}
export enum VoiceOPCodes {
IDENTIFY = 0,
SELECT_PROTOCOL = 1,
READY = 2,
HEARTBEAT = 3,
SELECT_PROTOCOL_ACK = 4,
SPEAKING = 5,
HEARTBEAT_ACK = 6,
RESUME = 7,
HELLO = 8,
RESUMED = 9,
VIDEO = 12,
CLIENT_DISCONNECT = 13,
SESSION_UPDATE = 14,
MEDIA_SINK_WANTS = 15,
VOICE_BACKEND_VERSION = 16,
CHANNEL_OPTIONS_UPDATE = 17
}

View File

@@ -0,0 +1,51 @@
import { WebSocket } from "@fosscord/gateway";
import MediaServer, { IncomingStream, OutgoingStream, Transport } from "medooze-media-server";
import SemanticSDP from "semantic-sdp";
MediaServer.enableLog(true);
export const PublicIP = process.env.PUBLIC_IP || "127.0.0.1";
try {
const range = process.env.WEBRTC_PORT_RANGE || "4000";
var ports = range.split("-");
const min = Number(ports[0]);
const max = Number(ports[1]);
MediaServer.setPortRange(min, max);
} catch (error) {
console.error("Invalid env var: WEBRTC_PORT_RANGE", process.env.WEBRTC_PORT_RANGE, error);
process.exit(1);
}
export const endpoint = MediaServer.createEndpoint(PublicIP);
export const channels = new Map<string, Set<Client>>();
export interface Client {
transport?: Transport;
websocket: WebSocket;
out: {
stream?: OutgoingStream;
tracks: Map<
string,
{
audio_ssrc: number;
video_ssrc: number;
rtx_ssrc: number;
}
>;
};
in: {
stream?: IncomingStream;
audio_ssrc: number;
video_ssrc: number;
rtx_ssrc: number;
};
sdp: SemanticSDP.SDPInfo;
channel_id: string;
}
export function getClients(channel_id: string) {
if (!channels.has(channel_id)) channels.set(channel_id, new Set());
return channels.get(channel_id)!;
}

2
src/webrtc/util/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./Constants";
export * from "./MediaServer";

View File

@@ -72,6 +72,7 @@
"@fosscord/gateway": ["./gateway/index"],
"@fosscord/cdn": ["./cdn/index"],
"@fosscord/util": ["./util/index"]
"@fosscord/webrtc": ["./webrtc/index"]
},
"noEmitHelpers": true,
"importHelpers": true

7063
yarn.lock Normal file

File diff suppressed because it is too large Load Diff