Merge branch 'develop' into 'master'
Develop -> Master See merge request knotteye/satyr!29pull/2/head^2 v0.10.0
commit
4d36c2c429
|
@ -4,3 +4,4 @@ config/**/*
|
|||
!config/.gitkeep
|
||||
install/db_setup.sql
|
||||
build/**
|
||||
site/templates.js
|
10
README.md
10
README.md
|
@ -13,7 +13,15 @@ Follow the instructions after setup runs.
|
|||
|
||||
### Run the server
|
||||
```bash
|
||||
npm start
|
||||
npm run start
|
||||
```
|
||||
You can also skip checking the database version and compiling templates (if you don't use server-side rendering) on startup.
|
||||
```bash
|
||||
npm run start -- --skip-migrate --skip-compile
|
||||
# don't forget to migrate manually when you update
|
||||
npm run migrate
|
||||
# and compile templates after any changes
|
||||
npm run make-templates
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
|
14
docs/REST.md
14
docs/REST.md
|
@ -77,7 +77,9 @@ The array will be wrapped in a JSON object under the key 'users'.
|
|||
|
||||
## /api/users/all
|
||||
|
||||
Same as above, but returns all users regardless of whether they are streaming. Also unfinished.
|
||||
Same as above, but returns all users regardless of whether they are streaming and if they're streaming or not.
|
||||
|
||||
**Example**: `{users: [{username:"foo", title:"bar", live:1}] }`
|
||||
|
||||
|
||||
|
||||
|
@ -89,7 +91,9 @@ Register a new user.
|
|||
|
||||
**Authentication**: no
|
||||
|
||||
**Parameters**: Username, password, confirm
|
||||
**Parameters**: Username, password, confirm, invite(optional)
|
||||
|
||||
Invite is an optional invite code to bypass disabled registration.
|
||||
|
||||
**Response**: If successful, returns a json object with the users stream key. Otherwise returns `{error: "error reason"}`
|
||||
|
||||
|
@ -111,7 +115,7 @@ Obtain a signed json web token for authentication
|
|||
|
||||
**Response**: If succesful, will return `{success: ""}` or `{success: "already verified"}` if the JWT provided is too early to be renewed. If unsuccesful, will return `{error: "invalid password"}` or `{error: "Username or Password Incorrect"}` depending on the authentication method. Note that if a JWT is available, the parameters will be ignored.
|
||||
|
||||
**Notes**: I've already listed nearly every response. My final note is that the JWT is set as the cookie 'Authorization', not returned in the response.
|
||||
**Notes**: The returned JWT is set as the cookie httponly 'Authorization'. It will also return a non httponly cookie X-Auth-As with the username of the authenticated user.
|
||||
|
||||
|
||||
## /api/user/update
|
||||
|
@ -122,9 +126,9 @@ Update the current user's information
|
|||
|
||||
**Authentication**: yes
|
||||
|
||||
**Parameters**: title, bio, rec
|
||||
**Parameters**: title, bio, rec, twitch, twitch_key
|
||||
|
||||
Rec is a boolean (whether to record VODs), others are strings. Parameters that are not included in the request will not be updated.
|
||||
Rec is a boolean (whether to record VODs), twitch is a boolean (whether to mirror video streams to twitch) others are strings. Twitch_key is the stream key to use for twitch. Parameters that are not included in the request will not be updated.
|
||||
|
||||
**Response**: Returns `{error: "error code"}` or `{success: ""}`
|
||||
|
||||
|
|
|
@ -9,11 +9,15 @@ media:
|
|||
ffmpeg: '<ffmpeg>'
|
||||
|
||||
rtmp:
|
||||
# enable cluster mode this will pretty much entirely
|
||||
# break the ability to play rtmp for clients
|
||||
cluster: false
|
||||
port: 1935
|
||||
|
||||
http:
|
||||
# uncomment to set HSTS when SSL is ready
|
||||
#hsts: true
|
||||
server_side_render: false
|
||||
|
||||
database:
|
||||
user: '<dbuser>'
|
||||
|
@ -57,3 +61,11 @@ chat:
|
|||
username:
|
||||
#https://twitchapps.com/tmi/
|
||||
password:
|
||||
|
||||
twitch_mirror:
|
||||
# enable to allow users to mirror video streams to twitch
|
||||
# for those with truly no bandwidth limits
|
||||
enabled: false
|
||||
# https://stream.twitch.tv/ingests/
|
||||
# do not include {stream_key}
|
||||
ingest: 'rtmp://live-ord02.twitch.tv/app/
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "satyr",
|
||||
"version": "0.7.2",
|
||||
"version": "0.10.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -18,9 +18,9 @@
|
|||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz",
|
||||
"integrity": "sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w=="
|
||||
"version": "12.12.67",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.67.tgz",
|
||||
"integrity": "sha512-R48tgL2izApf+9rYNH+3RBMbRpPeW3N8f0I9HMhggeq4UXwBDqumJ14SDs4ctTMhG11pIOduZ4z3QWGOiMc9Vg=="
|
||||
},
|
||||
"a-sync-waterfall": {
|
||||
"version": "1.0.1",
|
||||
|
@ -149,12 +149,12 @@
|
|||
"integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI="
|
||||
},
|
||||
"bcrypt": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.6.tgz",
|
||||
"integrity": "sha512-taA5bCTfXe7FUjKroKky9EXpdhkVvhE5owfxfLYodbrAR1Ul3juLmIQmIQBK4L9a5BuUcE6cqmwT+Da20lF9tg==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz",
|
||||
"integrity": "sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==",
|
||||
"requires": {
|
||||
"nan": "2.13.2",
|
||||
"node-pre-gyp": "0.12.0"
|
||||
"node-addon-api": "^3.0.0",
|
||||
"node-pre-gyp": "0.15.0"
|
||||
}
|
||||
},
|
||||
"better-assert": {
|
||||
|
@ -278,9 +278,9 @@
|
|||
}
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz",
|
||||
"integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A=="
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
|
@ -773,9 +773,9 @@
|
|||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
|
||||
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
|
@ -861,9 +861,9 @@
|
|||
}
|
||||
},
|
||||
"ignore-walk": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.2.tgz",
|
||||
"integrity": "sha512-EXyErtpHbn75ZTsOADsfx6J/FPo6/5cjev46PXrcTpd8z3BoRkXgYu9/JVqrI7tusjmwCZutGeRJeU0Wo1e4Cw==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
|
||||
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
|
@ -950,6 +950,11 @@
|
|||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"optional": true
|
||||
},
|
||||
"is-port-available": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/is-port-available/-/is-port-available-0.1.5.tgz",
|
||||
"integrity": "sha512-/r7UZAQtfgDFdhxzM71jG0mkC4oSRA513cImMILdRe/+UOIe0Se/D/Z7XCua4AFg5k4Zt3ALMGaC1W3FzlrR2w=="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
|
@ -964,9 +969,9 @@
|
|||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
},
|
||||
"lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
|
@ -1042,21 +1047,26 @@
|
|||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.6.5.tgz",
|
||||
"integrity": "sha512-ewSKOPFH9blOLXx0YSE+mbrNMBFPS+11a2b03QZ+P4LVrUHW/GAlqeYC7DBknDyMWkHzrzTpDhUvy7MUxqyrPA==",
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
|
||||
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.2.tgz",
|
||||
"integrity": "sha512-hR3At21uSrsjjDTWrbu0IMLTpnkpv8IIMFDFaoz43Tmu4LkmAXfH44vNNzpTnf+OAQQCHrb91y/wc2J4x5XgSQ==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
|
||||
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
|
||||
"requires": {
|
||||
"minipass": "^2.2.1"
|
||||
"minipass": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
|
@ -1065,13 +1075,6 @@
|
|||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
|
@ -1098,12 +1101,13 @@
|
|||
"nan": {
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
|
||||
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw=="
|
||||
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
|
||||
"optional": true
|
||||
},
|
||||
"needle": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz",
|
||||
"integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==",
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz",
|
||||
"integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==",
|
||||
"requires": {
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.4.4",
|
||||
|
@ -1115,6 +1119,11 @@
|
|||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"node-addon-api": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.2.tgz",
|
||||
"integrity": "sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg=="
|
||||
},
|
||||
"node-icu-charset-detector": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/node-icu-charset-detector/-/node-icu-charset-detector-0.2.0.tgz",
|
||||
|
@ -1125,40 +1134,47 @@
|
|||
}
|
||||
},
|
||||
"node-media-server": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.1.3.tgz",
|
||||
"integrity": "sha512-BZf39fpVDSVQT2E+8DqSVOb7oo31rcbA36l9sqtSuyZhBdxjidL5Nk2/G/2vqMGR9Q4JKzkTskGay2dWy5ZsUQ==",
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.2.4.tgz",
|
||||
"integrity": "sha512-2Y5hZ+BI2YxM5+PiEXM9isAZUPSJoENTb0xXVzg8MzP9nFtVVv+X7+iGnFeyXB0BWaCsdBFD5A/rTL4dfaCw+Q==",
|
||||
"requires": {
|
||||
"basic-auth-connect": "^1.0.0",
|
||||
"chalk": "^2.4.2",
|
||||
"dateformat": "^3.0.3",
|
||||
"express": "^4.16.4",
|
||||
"lodash": ">=4.17.13",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mkdirp": "1.0.3",
|
||||
"ws": "^5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz",
|
||||
"integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-pre-gyp": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz",
|
||||
"integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz",
|
||||
"integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==",
|
||||
"requires": {
|
||||
"detect-libc": "^1.0.2",
|
||||
"mkdirp": "^0.5.1",
|
||||
"needle": "^2.2.1",
|
||||
"mkdirp": "^0.5.3",
|
||||
"needle": "^2.5.0",
|
||||
"nopt": "^4.0.1",
|
||||
"npm-packlist": "^1.1.6",
|
||||
"npmlog": "^4.0.2",
|
||||
"rc": "^1.2.7",
|
||||
"rimraf": "^2.6.1",
|
||||
"semver": "^5.3.0",
|
||||
"tar": "^4"
|
||||
"tar": "^4.4.2"
|
||||
}
|
||||
},
|
||||
"nopt": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
|
||||
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
|
||||
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
|
||||
"requires": {
|
||||
"abbrev": "1",
|
||||
"osenv": "^0.1.4"
|
||||
|
@ -1171,17 +1187,26 @@
|
|||
"optional": true
|
||||
},
|
||||
"npm-bundled": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz",
|
||||
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g=="
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
|
||||
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
|
||||
"requires": {
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npm-normalize-package-bin": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
|
||||
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
|
||||
},
|
||||
"npm-packlist": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.4.tgz",
|
||||
"integrity": "sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==",
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
|
||||
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
|
||||
"requires": {
|
||||
"ignore-walk": "^3.0.1",
|
||||
"npm-bundled": "^1.0.1"
|
||||
"npm-bundled": "^1.0.1",
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npmlog": {
|
||||
|
@ -1368,13 +1393,6 @@
|
|||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
|
@ -1505,9 +1523,9 @@
|
|||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
||||
},
|
||||
"simple-websocket": {
|
||||
"version": "9.0.0",
|
||||
|
@ -1790,17 +1808,6 @@
|
|||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"minipass": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
|
||||
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"to-array": {
|
||||
|
@ -1905,9 +1912,9 @@
|
|||
"integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
|
||||
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
||||
},
|
||||
"yeast": {
|
||||
"version": "0.1.2",
|
||||
|
|
16
package.json
16
package.json
|
@ -1,20 +1,23 @@
|
|||
{
|
||||
"name": "satyr",
|
||||
"version": "0.9.2",
|
||||
"version": "0.10.0",
|
||||
"description": "A livestreaming server.",
|
||||
"license": "AGPL-3.0",
|
||||
"author": "knotteye",
|
||||
"scripts": {
|
||||
"start": "ts-node src/index.ts",
|
||||
"user": "ts-node src/cli.ts",
|
||||
"setup": "sh install/setup.sh"
|
||||
"cli": "ts-node src/cli.ts",
|
||||
"setup": "sh install/setup.sh",
|
||||
"migrate": "ts-node src/migrate.ts",
|
||||
"invite": "ts-node src/cli.ts --invite",
|
||||
"make-templates": "nunjucks-precompile -i [\"\\.html$\",\"\\.njk$\"] templates > site/templates.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitlab.com/knotteye/satyr.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^3.0.6",
|
||||
"bcrypt": "^5.0.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"cookie-parser": "^1.4.4",
|
||||
"dank-twitch-irc": "^3.2.6",
|
||||
|
@ -23,9 +26,10 @@
|
|||
"express": "^4.17.1",
|
||||
"flags": "^0.1.3",
|
||||
"irc": "^0.5.2",
|
||||
"is-port-available": "^0.1.5",
|
||||
"jose": "^1.15.1",
|
||||
"mysql": "^2.17.1",
|
||||
"node-media-server": ">=2.1.3 <3.0.0",
|
||||
"node-media-server": "^2.2.4",
|
||||
"nunjucks": "^3.2.1",
|
||||
"parse-yaml": "^0.1.0",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
|
@ -36,6 +40,6 @@
|
|||
"typescript": "^3.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.7.5"
|
||||
"@types/node": "^12.12.67"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="/styles.css">
|
||||
<link rel="stylesheet" type="text/css" href="/local.css">
|
||||
<link rel="icon" type="image/svg" href="/logo.svg">
|
||||
<script src="/nunjucks-slim.js"></script>
|
||||
<script src="/templates.js"></script>
|
||||
<script>
|
||||
nunjucks.configure({ autoescape: true });
|
||||
</script>
|
||||
<script>
|
||||
//should check for and refresh login tokens on pageload..
|
||||
if(document.cookie.match(/^(.*;)?\s*X-Auth-As\s*=\s*[^;]+(.*)?$/) !== null) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/api/login", true);
|
||||
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
xhr.send("");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="render(window.location.pathname)">
|
||||
<script src="/index.js"></script>
|
||||
</body>
|
|
@ -0,0 +1,170 @@
|
|||
async function render(path){
|
||||
var context = await getContext();
|
||||
switch(path){
|
||||
//nothing but context
|
||||
case (path.match(/^\/about\/?$/) || {}).input:
|
||||
document.body.innerHTML = nunjucks.render('about.njk', context);
|
||||
modifyLinks();
|
||||
break;
|
||||
case (path.match(/^\/login\/?$/) || {}).input:
|
||||
document.body.innerHTML = nunjucks.render('login.njk', context);
|
||||
modifyLinks();
|
||||
break;
|
||||
case (path.match(/^\/register\/?$/) || {}).input:
|
||||
if(!context.registration) window.location = '/';
|
||||
document.body.innerHTML = nunjucks.render('registration.njk', context);
|
||||
modifyLinks();
|
||||
break;
|
||||
case (path.match(/^\/changepwd\/?$/) || {}).input:
|
||||
document.body.innerHTML = nunjucks.render('changepwd.njk', context);
|
||||
modifyLinks();
|
||||
break;
|
||||
case (path.match(/^\/chat\/?$/) || {}).input:
|
||||
document.body.innerHTML = nunjucks.render('chat.html', context);
|
||||
modifyLinks();
|
||||
break;
|
||||
case (path.match(/^\/help\/?$/) || {}).input:
|
||||
document.body.innerHTML = nunjucks.render('help.njk', context);
|
||||
modifyLinks();
|
||||
break;
|
||||
//need to hit the API
|
||||
case (path.match(/^\/users\/live\/?$/) || {}).input:
|
||||
var list = JSON.parse(await makeRequest("POST", "/api/users/live", JSON.stringify({num: 50})));
|
||||
document.body.innerHTML = nunjucks.render('live.njk', Object.assign({list: list.users}, context));
|
||||
modifyLinks();
|
||||
break;
|
||||
case (path.match(/^\/users\/?$/) || {}).input:
|
||||
var list = JSON.parse(await makeRequest("POST", "/api/users/all", JSON.stringify({num: 50})));
|
||||
document.body.innerHTML = nunjucks.render('list.njk', Object.assign({list: list.users}, context));
|
||||
modifyLinks();
|
||||
break;
|
||||
case (path.match(/^\/profile\/chat\/?$/) || {}).input:
|
||||
if(!context.auth.name) window.location = '/login';
|
||||
var config = JSON.parse(await makeRequest("GET", '/api/'+context.auth.name+'/config'));
|
||||
config = {
|
||||
integ: {
|
||||
twitch: config.twitch,
|
||||
xmpp: config.xmpp,
|
||||
irc: config.irc,
|
||||
discord: config.discord
|
||||
}
|
||||
};
|
||||
document.body.innerHTML = nunjucks.render('chat_integ.njk', Object.assign(config, context));
|
||||
modifyLinks();
|
||||
break;
|
||||
case (path.match(/^\/profile\/?$/) || {}).input:
|
||||
if(!context.auth.name) window.location = '/login';
|
||||
var config = JSON.parse(await makeRequest("GET", '/api/'+context.auth.name+'/config'));
|
||||
config = {
|
||||
meta: {
|
||||
title: config.title,
|
||||
about: config.about
|
||||
},
|
||||
rflag: {record_flag: config.record_flag},
|
||||
twitch: config.twitch_mirror
|
||||
};
|
||||
document.body.innerHTML = nunjucks.render('profile.njk', Object.assign(config, context));
|
||||
modifyLinks();
|
||||
break;
|
||||
//parsing slugs
|
||||
case (path.match(/^\/invite\//) || {}).input: // /invite/:code
|
||||
document.body.innerHTML = nunjucks.render('invite.njk', Object.assign({icode: path.substring(8)}, context));
|
||||
modifyLinks();
|
||||
break;
|
||||
//slugs and API
|
||||
case (path.match(/^\/users\/.+\/?$/) || {}).input: // /users/:user
|
||||
if(path.substring(path.length - 1).indexOf('/') !== -1)
|
||||
var usr = path.substring(7, path.length - 1);
|
||||
else var usr = path.substring(7);
|
||||
var config = JSON.parse(await makeRequest("GET", '/api/'+usr+'/config'));
|
||||
if(!config.title){document.body.innerHTML = nunjucks.render('404.njk', context); break;}
|
||||
document.body.innerHTML = nunjucks.render('user.njk', Object.assign({about: config.about, title: config.title, username: config.username}, context));
|
||||
modifyLinks();
|
||||
break;
|
||||
case (path.match(/^\/vods\/.+\/manage\/?$/) || {}).input: // /vods/:user/manage
|
||||
var usr = path.substring(6, (path.length - 7));
|
||||
if(context.auth.name !== usr) window.location = '/vods/'+usr;
|
||||
var vods = JSON.parse(await makeRequest("GET", '/api/'+usr+'/vods'));
|
||||
document.body.innerHTML = nunjucks.render('managevods.njk', Object.assign({user: usr, list: vods.vods.filter(fn => fn.name.endsWith('.mp4'))}, context));
|
||||
modifyLinks();
|
||||
break;
|
||||
case (path.match(/^\/vods\/.+\/?$/) || {}).input: // /vods/:user
|
||||
if(path.substring(path.length - 1).indexOf('/') !== -1)
|
||||
var usr = path.substring(6, path.length - 1);
|
||||
else var usr = path.substring(6);
|
||||
var vods = JSON.parse(await makeRequest("GET", '/api/'+usr+'/vods'));
|
||||
document.body.innerHTML = nunjucks.render('vods.njk', Object.assign({user: usr, list: vods.vods.filter(fn => fn.name.endsWith('.mp4'))}, context));
|
||||
modifyLinks();
|
||||
break;
|
||||
//root
|
||||
case "/":
|
||||
render('/users/live');
|
||||
break;
|
||||
case "":
|
||||
render('/users/live');
|
||||
break;
|
||||
//404
|
||||
default:
|
||||
document.body.innerHTML = nunjucks.render('404.njk', context);
|
||||
modifyLinks();
|
||||
}
|
||||
}
|
||||
|
||||
async function getContext(){
|
||||
var info = JSON.parse(await makeRequest('GET', '/api/instance/info'));
|
||||
info.sitename = info.name;
|
||||
info.name = null;
|
||||
info.auth = {
|
||||
is: document.cookie.match(/^(.*;)?\s*X-Auth-As\s*=\s*[^;]+(.*)?$/) !== null,
|
||||
name: parseCookie(document.cookie)['X-Auth-As']
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
function makeRequest(method, url, payload) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open(method, url);
|
||||
xhr.onload = function () {
|
||||
if (this.status >= 200 && this.status < 300) {
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
reject({
|
||||
status: this.status,
|
||||
statusText: xhr.statusText
|
||||
});
|
||||
}
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
reject({
|
||||
status: this.status,
|
||||
statusText: xhr.statusText
|
||||
});
|
||||
};
|
||||
!payload ? xhr.send() : xhr.send(payload);
|
||||
});
|
||||
}
|
||||
|
||||
function parseCookie(c){
|
||||
if(typeof(c) !== 'string' || !c.includes('=')) return {};
|
||||
return Object.assign({[c.split('=')[0].trim()]:c.split('=')[1].split(';')[0].trim()}, parseCookie(c.split(/;(.+)/)[1]));
|
||||
}
|
||||
|
||||
function handleLoad() {
|
||||
var r = JSON.parse(document.getElementById('responseFrame').contentDocument.documentElement.textContent).success
|
||||
if (typeof(r) !== 'undefined') window.location.href = '/profile'
|
||||
}
|
||||
|
||||
function modifyLinks() {
|
||||
for (var ls = document.links, numLinks = ls.length, i=0; i<numLinks; i++){
|
||||
if(ls[i].href.indexOf(location.protocol+'//'+location.host) !== -1) {
|
||||
//should be a regular link
|
||||
ls[i].setAttribute('onclick', 'return internalLink(\"'+ls[i].href.substring((location.protocol+'//'+location.host).length)+'\")');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function internalLink(path){
|
||||
this.render(path);
|
||||
return false;
|
||||
}
|
43
src/api.ts
43
src/api.ts
|
@ -1,9 +1,10 @@
|
|||
import * as db from "./database";
|
||||
import * as base64id from "base64id";
|
||||
import { config } from "./config";
|
||||
import {unlink} from "fs";
|
||||
|
||||
async function register(name: string, password: string, confirm: string): Promise<object> {
|
||||
if(!config['satyr']['registration']) return {"error":"registration disabled"};
|
||||
async function register(name: string, password: string, confirm: string, invite?: boolean): Promise<object> {
|
||||
if(!config['satyr']['registration'] && !invite) return {"error":"registration disabled"};
|
||||
if(name.includes(';') || name.includes(' ') || name.includes('\'')) return {"error":"illegal characters"};
|
||||
if(password !== confirm) return {"error":"mismatched passwords"};
|
||||
for(let i=0;i<config['satyr']['restrictedNames'].length;i++){
|
||||
|
@ -18,7 +19,7 @@ async function register(name: string, password: string, confirm: string): Promis
|
|||
}
|
||||
|
||||
async function update(fields: object): Promise<object>{
|
||||
if(!fields['title'] && !fields['bio'] && (fields['rec'] !== 'true' && fields['rec'] !== 'false')) return {"error":"no valid fields specified"};
|
||||
if(!fields['title'] && !fields['bio'] && (fields['rec'] !== 'true' && fields['rec'] !== 'false') && (fields['twitch'] !== 'true' && fields['twitch'] !== 'false') && !fields['twitch_key']) return {"error":"no valid fields specified"};
|
||||
let qs: string = "";
|
||||
let f: boolean = false;
|
||||
if(fields['title']) {qs += ' user_meta.title='+db.raw.escape(fields['title']);f = true;}
|
||||
|
@ -30,8 +31,19 @@ async function update(fields: object): Promise<object>{
|
|||
if(typeof(fields['rec']) === 'boolean' || typeof(fields['rec']) === 'number') {
|
||||
if(f) qs+=',';
|
||||
qs += ' users.record_flag='+db.raw.escape(fields['rec']);
|
||||
f=true;
|
||||
}
|
||||
await db.query('UPDATE users,user_meta SET'+qs+' WHERE users.username='+db.raw.escape(fields['name'])+' AND user_meta.username='+db.raw.escape(fields['name']));
|
||||
if(typeof(fields['twitch']) === 'boolean' || typeof(fields['twitch']) === 'number') {
|
||||
if(f) qs+=',';
|
||||
qs += ' twitch_mirror.enabled='+db.raw.escape(fields['twitch']);
|
||||
f=true;
|
||||
}
|
||||
if(fields['twitch_key']){
|
||||
if(f) qs+=',';
|
||||
qs += ' twitch_mirror.twitch_key='+db.raw.escape(fields['twitch_key']);
|
||||
f = true;
|
||||
}
|
||||
await db.query('UPDATE users,user_meta,twitch_mirror SET'+qs+' WHERE users.username='+db.raw.escape(fields['name'])+' AND user_meta.username='+db.raw.escape(fields['name'])+' AND twitch_mirror.username='+db.raw.escape(fields['name']));
|
||||
return {success:""};
|
||||
}
|
||||
|
||||
|
@ -75,9 +87,11 @@ async function getConfig(username: string, all?: boolean): Promise<object>{
|
|||
let users = await db.query('SELECT stream_key,record_flag FROM users WHERE username='+db.raw.escape(username));
|
||||
if(users[0]) Object.assign(t, users[0]);
|
||||
let usermeta = await db.query('SELECT title,about FROM user_meta WHERE username='+db.raw.escape(username));
|
||||
if(usermeta[0]) Object.assign(t, users[0]);
|
||||
if(usermeta[0]) Object.assign(t, usermeta[0]);
|
||||
let ci = await db.query('SELECT irc,xmpp,twitch,discord FROM chat_integration WHERE username='+db.raw.escape(username));
|
||||
if(ci[0]) Object.assign(t, ci[0]);
|
||||
let tw = await db.query('SELECT enabled,twitch_key FROM twitch_mirror WHERE username='+db.raw.escape(username));
|
||||
if(tw[0]) t['twitch_mirror'] = Object.assign({}, tw[0]);
|
||||
}
|
||||
else {
|
||||
let um = await db.query('SELECT title,about FROM user_meta WHERE username='+db.raw.escape(username));
|
||||
|
@ -86,4 +100,21 @@ async function getConfig(username: string, all?: boolean): Promise<object>{
|
|||
return t;
|
||||
}
|
||||
|
||||
export { register, update, changepwd, changesk, login, updateChat, deleteVODs, getConfig };
|
||||
async function genInvite(): Promise<string>{
|
||||
var invitecode: string = base64id.generateId();
|
||||
await db.query('INSERT INTO invites (code) VALUES (\"'+invitecode+'\")');
|
||||
return invitecode;
|
||||
}
|
||||
|
||||
async function validInvite(code: string): Promise<boolean>{
|
||||
if(typeof(code) !== "string" || code === "") return false;
|
||||
var result = await db.query('SELECT code FROM invites WHERE code='+db.raw.escape(code));
|
||||
if(!result[0] || result[0]['code'] !== code) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function useInvite(code: string): Promise<void>{
|
||||
if(validInvite(code)) await db.query('DELETE FROM invites WHERE code='+db.raw.escape(code));
|
||||
}
|
||||
|
||||
export { register, update, changepwd, changesk, login, updateChat, deleteVODs, getConfig, genInvite, useInvite, validInvite };
|
|
@ -1,10 +1,52 @@
|
|||
import * as db from "./database";
|
||||
import {readdirSync} from "fs";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
async function init() {
|
||||
if(process.argv.indexOf('--skip-migrate') === -1){
|
||||
console.log('Checking database version.');
|
||||
var tmp: string[] = await db.query('show tables like \"db_meta\"');
|
||||
if(tmp.length === 0){
|
||||
console.log('No database version info, running initial migration.');
|
||||
await require('./db/0').run();
|
||||
await bringUpToDate();
|
||||
}
|
||||
else {
|
||||
await bringUpToDate();
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('Skipping database version check.');
|
||||
}
|
||||
|
||||
if(!require('./config').config['http']['server_side_render'] && process.argv.indexOf('--skip-compile') === -1) {
|
||||
console.log("Compiling templates for client-side frontend.");
|
||||
execSync(process.cwd()+'/node_modules/.bin/nunjucks-precompile -i [\"\\.html$\",\"\\.njk$\"] templates > site/templates.js');
|
||||
}
|
||||
else if(!require('./config').config['http']['server_side_render']){
|
||||
console.log("Skipped compiling templates for client-side frontend.");
|
||||
}
|
||||
|
||||
//If satyr is restarted in the middle of a stream
|
||||
//it causes problems
|
||||
//Live flags in the database stay live
|
||||
await db.query('update user_meta set live=false');
|
||||
}
|
||||
|
||||
async function bringUpToDate(): Promise<void>{
|
||||
var versions: Object[] = await db.query('select * from db_meta');
|
||||
var scripts: Buffer[] | string[] = readdirSync('./src/db/', {withFileTypes: false});
|
||||
var diff: number = scripts.length - versions.length
|
||||
if(diff === 0){
|
||||
console.log('No migration needed.');
|
||||
} else {
|
||||
console.log('Versions differ, migrating now.');
|
||||
for(let i=0;i<diff;i++){
|
||||
console.log('Migration to version '+Math.floor(scripts.length-(diff-i)));
|
||||
await require('./db/'+scripts[Math.floor(scripts.length-(diff-i))]).run();
|
||||
}
|
||||
console.log('Done migrating database.');
|
||||
}
|
||||
}
|
||||
|
||||
export { init };
|
||||
|
|
13
src/cli.ts
13
src/cli.ts
|
@ -1,4 +1,5 @@
|
|||
import * as db from "./database"
|
||||
import * as db from "./database";
|
||||
import * as api from "./api";
|
||||
import * as flags from "flags";
|
||||
|
||||
db.init();
|
||||
|
@ -6,6 +7,7 @@ db.init();
|
|||
flags.defineString('adduser', '', 'User to add');
|
||||
flags.defineString('rmuser', '', 'User to remove');
|
||||
flags.defineString('password', '', 'password to hash');
|
||||
flags.defineBoolean('invite', false, 'generate invite code');
|
||||
|
||||
flags.parse();
|
||||
|
||||
|
@ -24,3 +26,12 @@ if(flags.get('rmuser') !== ''){
|
|||
process.exit();
|
||||
});
|
||||
}
|
||||
|
||||
if(flags.get('invite')){
|
||||
var config = require("./config").config;
|
||||
api.genInvite().then((r: string) => {
|
||||
console.log('invite code: '+r);
|
||||
console.log('Direct the user to https://'+config['satyr']['domain']+'/invite/'+r);
|
||||
process.exit();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,289 @@
|
|||
import * as cluster from 'cluster';
|
||||
import * as net from 'net';
|
||||
import * as NodeRtmpSession from '../node_modules/node-media-server/node_rtmp_session';
|
||||
import * as logger from '../node_modules/node-media-server/node_core_logger';
|
||||
import * as dirty from "dirty";
|
||||
import { mkdir, fstat, access } from "fs";
|
||||
import * as strf from "strftime";
|
||||
import * as ctx from '../node_modules/node-media-server/node_core_ctx';
|
||||
import * as db from "./database";
|
||||
import {config} from "./config";
|
||||
import * as isPortAvailable from "is-port-available";
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
const { exec, execFile } = require('child_process');
|
||||
|
||||
const keystore = dirty();
|
||||
const num_processes = require('os').cpus().length;
|
||||
const workerMap = {};
|
||||
|
||||
if (cluster.isMaster) {
|
||||
//master logic
|
||||
|
||||
//store workers in here
|
||||
var workers = [];
|
||||
|
||||
// Helper function for spawning worker at index 'i'.
|
||||
var spawn = function(i) {
|
||||
workers[i] = cluster.fork();
|
||||
workers[i].on('message', (msg) => {
|
||||
handleMsgMaster(msg, i)
|
||||
});
|
||||
|
||||
// Restart worker on exit
|
||||
workers[i].on('exit', function(code, signal) {
|
||||
console.log('[RTMP Cluster MASTER] Respawning Worker', i);
|
||||
spawn(i);
|
||||
});
|
||||
};
|
||||
|
||||
// Spawn initial workers
|
||||
for (var i = 0; i < num_processes; i++) {
|
||||
spawn(i);
|
||||
}
|
||||
|
||||
var nextWorker: number = 0;
|
||||
|
||||
//TODO assign incoming connections correctly
|
||||
|
||||
var server = net.createServer({ pauseOnConnect: true }, function(connection) {
|
||||
if(nextWorker >= workers.length) nextWorker = 0;
|
||||
var worker = workers[nextWorker];
|
||||
worker.send('rtmp-session:connection', connection); //send connection to worker
|
||||
}).listen(config['rtmp']['port']);
|
||||
|
||||
console.log('[RTMP Cluster MASTER] Master Ready.');
|
||||
} else {
|
||||
|
||||
//worker logic
|
||||
|
||||
//we need our own database pool since we can't share memory anyone else
|
||||
db.initRTMPCluster();
|
||||
|
||||
const rtmpcfg = {
|
||||
logType: 0,
|
||||
rtmp: Object.assign({port: 1936}, config['rtmp'])
|
||||
};
|
||||
|
||||
//find a unique port to listen on
|
||||
getPort().then((wPort) => {
|
||||
|
||||
// creating the rtmp server
|
||||
var serv = net.createServer((socket) => {
|
||||
let session = new NodeRtmpSession(rtmpcfg, socket);
|
||||
session.run();
|
||||
}).listen(wPort);
|
||||
logger.setLogType(0);
|
||||
|
||||
// RTMP Server Logic
|
||||
newRTMPListener('postPublish', (id, StreamPath, args) =>{
|
||||
console.log(`[RTMP Cluster WORKER ${process.pid}] Publish Hook for stream: ${id}`);
|
||||
let session = getRTMPSession(id);
|
||||
let app: string = StreamPath.split("/")[1];
|
||||
let key: string = StreamPath.split("/")[2];
|
||||
//disallow urls not formatted exactly right
|
||||
if (StreamPath.split("/").length !== 3 || key.includes(' ')){
|
||||
console.log(`[RTMP Cluster WORKER ${process.pid}] Malformed URL, closing connection for stream: ${id}`);
|
||||
session.reject();
|
||||
return false;
|
||||
}
|
||||
if(app !== config['media']['privateEndpoint']){
|
||||
//app isn't at public endpoint if we've reached this point
|
||||
console.log(`[RTMP Cluster WORKER ${process.pid}] Wrong endpoint, rejecting stream: ${id}`);
|
||||
session.reject();
|
||||
return false;
|
||||
}
|
||||
//if the url is formatted correctly and the user is streaming to the correct private endpoint
|
||||
//grab the username from the database and redirect the stream there if the key is valid
|
||||
//otherwise kill the session
|
||||
db.query('select username,record_flag from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
|
||||
if(results[0]){
|
||||
//transcode to mpd after making sure directory exists
|
||||
keystore[results[0].username] = key;
|
||||
mkdir(config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username, { recursive : true }, ()=>{;});
|
||||
while(true){
|
||||
if(session.audioCodec !== 0 && session.videoCodec !== 0){
|
||||
transCommand(results[0].username, key, wPort).then((r) => {
|
||||
execFile(config['media']['ffmpeg'], r, {maxBuffer: Infinity}, (err, stdout, stderr) => {
|
||||
/*console.log(err);
|
||||
console.log(stdout);
|
||||
console.log(stderr);*/
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
await sleep(300);
|
||||
}
|
||||
if(results[0].record_flag && config['media']['record']){
|
||||
console.log(`[RTMP Cluster WORKER ${process.pid}] Initiating recording for stream: ${id}`);
|
||||
mkdir(config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username, { recursive : true }, (err) => {
|
||||
if (err) throw err;
|
||||
execFile(config['media']['ffmpeg'], ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+wPort+'/'+config['media']['privateEndpoint']+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], {
|
||||
detached : true,
|
||||
stdio : 'inherit',
|
||||
maxBuffer: Infinity
|
||||
}).unref();
|
||||
//spawn an ffmpeg process to record the stream, then detach it completely
|
||||
//ffmpeg can then (probably) finalize the recording if satyr crashes mid-stream
|
||||
});
|
||||
}
|
||||
else {
|
||||
console.log(`[RTMP Cluster WORKER ${process.pid}] Skipping recording for stream: ${id}`);
|
||||
}
|
||||
db.query('update user_meta set live=true where username=\''+results[0].username+'\' limit 1');
|
||||
db.query('SELECT twitch_key,enabled from twitch_mirror where username='+db.raw.escape(results[0].username)+' limit 1').then(async (tm) => {
|
||||
if(!tm[0]['enabled'] || !config['twitch_mirror']['enabled'] || !config['twitch_mirror']['ingest']) return;
|
||||
console.log('[NodeMediaServer] Mirroring to twitch for stream:',id)
|
||||
execFile(config['media']['ffmpeg'], ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+wPort+'/'+config['media']['privateEndpoint']+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', '-f', 'flv', config['twitch_mirror']['ingest']+tm[0]['twitch_key']], {
|
||||
detached: true,
|
||||
stdio : 'inherit',
|
||||
maxBuffer: Infinity
|
||||
}).unref();
|
||||
});
|
||||
console.log('[NodeMediaServer] Stream key ok for stream:',id);
|
||||
console.log(`[RTMP Cluster WORKER ${process.pid}] Stream key ok for stream: ${id}`);
|
||||
//notify master process that we're handling the stream for this user
|
||||
process.send({type: 'handle-publish', name:results[0].username});
|
||||
}
|
||||
else{
|
||||
console.log(`[RTMP Cluster WORKER ${process.pid}] Invalid stream key for stream: ${id}`);
|
||||
session.reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
newRTMPListener('donePublish', (id, StreamPath, args) => {
|
||||
let app: string = StreamPath.split("/")[1];
|
||||
let key: string = StreamPath.split("/")[2];
|
||||
if(app === config['media']['privateEndpoint']) {
|
||||
db.query('update user_meta,users set user_meta.live=false where users.stream_key='+db.raw.escape(key));
|
||||
db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
|
||||
if(results[0]) keystore.rm(results[0].username);
|
||||
//notify master process that we're no longer handling the stream for this user
|
||||
process.send({type: 'handle-publish-done', name:results[0].username});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
newRTMPListener('prePlay', (id, StreamPath, args) => {
|
||||
let session = getRTMPSession(id);
|
||||
let app: string = StreamPath.split("/")[1];
|
||||
let key: string = StreamPath.split("/")[2];
|
||||
//correctly formatted urls again
|
||||
if (StreamPath.split("/").length !== 3){
|
||||
console.log("[NodeMediaServer] Malformed URL, closing connection for stream:",id);
|
||||
session.reject();
|
||||
return false;
|
||||
}
|
||||
//localhost can play from whatever endpoint
|
||||
//other clients must use private endpoint
|
||||
if(app !== config['media']['publicEndpoint'] && !session.isLocal) {
|
||||
console.log("[NodeMediaServer] Non-local Play from private endpoint, rejecting client:",id);
|
||||
session.reject();
|
||||
return false;
|
||||
}
|
||||
//rewrite playpath to private endpoint serverside
|
||||
//(hopefully)
|
||||
if(app === config['media']['publicEndpoint']) {
|
||||
if(keystore[key]){
|
||||
session.playStreamPath = '/'+config['media']['privateEndpoint']+'/'+keystore[key];
|
||||
return true;
|
||||
}
|
||||
//here the client is asking for a valid stream that we don't have
|
||||
//so we are going to ask the master process for it
|
||||
else session.reject();
|
||||
}
|
||||
});
|
||||
|
||||
//recieve messages from master
|
||||
process.on('message', function(message, connection) {
|
||||
if (message === 'rtmp-session:connection') {
|
||||
// Emulate a connection event on the server by emitting the
|
||||
// event with the connection the master sent us.
|
||||
serv.emit('connection', connection);
|
||||
connection.resume();
|
||||
return;
|
||||
}
|
||||
if(message['type'] === 'stream-request:h') {
|
||||
if(!message['available'])
|
||||
getRTMPSession(message['id']).reject();
|
||||
}
|
||||
});
|
||||
console.log(`[RTMP Cluster WORKER ${process.pid}] Worker Ready.`);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function newRTMPListener(eventName, listener) {
|
||||
ctx.nodeEvent.on(eventName, listener);
|
||||
}
|
||||
|
||||
function getRTMPSession(id) {
|
||||
return ctx.sessions.get(id);
|
||||
}
|
||||
|
||||
async function getPort(): Promise<number>{
|
||||
let port = 1936+process.pid;
|
||||
while(true){
|
||||
let i=0;
|
||||
if(await isPortAvailable(port+i)){
|
||||
port += i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
async function transCommand(user: string, key: string, wPort): Promise<string[]>{
|
||||
let args: string[] = ['-loglevel', 'fatal', '-y'];
|
||||
if(config['transcode']['inputflags'] !== null && config['transcode']['inputflags'] !== "") args = args.concat(config['transcode']['inputflags'].split(" "));
|
||||
args = args.concat(['-i', 'rtmp://127.0.0.1:'+wPort+'/'+config['media']['privateEndpoint']+'/'+key, '-movflags', '+faststart']);
|
||||
if(config['transcode']['adaptive']===true && config['transcode']['variants'] > 1) {
|
||||
for(let i=0;i<config['transcode']['variants'];i++){
|
||||
args = args.concat(['-map', '0:2']);
|
||||
}
|
||||
args = args.concat(['-map', '0:1', '-c:a', 'aac', '-c:v:0', 'libx264']);
|
||||
for(let i=1;i<config['transcode']['variants'];i++){
|
||||
args = args.concat(['-c:v:'+i, 'libx264',]);
|
||||
}
|
||||
for(let i=1;i<config['transcode']['variants'];i++){
|
||||
let crf: number = Math.floor(18 + (i * 8)) > 51 ? 51 : Math.floor(18 + (i * 7));
|
||||
args = args.concat(['-crf:'+i, ''+crf]);
|
||||
}
|
||||
for(let i=1;i<config['transcode']['variants'];i++){
|
||||
let bv: number = Math.floor((5000 / config['transcode']['variants']) * (config['transcode']['variants'] - i));
|
||||
args = args.concat(['-b:v:'+i, ''+bv]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
args = args.concat(['-c:a', 'aac', '-c:v', 'libx264']);
|
||||
}
|
||||
args = args.concat(['-preset', 'veryfast', '-tune', 'zerolatency']);
|
||||
//if(config['transcode']['format'] === 'dash')
|
||||
args = args.concat(['-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30']);
|
||||
if(config['transcode']['outputflags'] !== null && config['transcode']['outputflags'] !== "") args = args.concat(config['transcode']['outputflags'].split(" "));
|
||||
args = args.concat(['-f', 'dash', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+user+'/index.mpd']);
|
||||
//else if(config['transcode']['format'] === 'hls')
|
||||
//args = args.concat(['-remove_at_exit', '1', '-hls_time', '1', '-hls_list_size', '30', '-f', 'hls', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+user+'/index.m3u8']);
|
||||
return args;
|
||||
}
|
||||
|
||||
function handleMsgMaster(msg, index) {
|
||||
if(msg['type'] === 'handle-publish'){
|
||||
workerMap[msg['name']] = index;
|
||||
nextWorker++;
|
||||
if(nextWorker >= workers.length) nextWorker = 0;
|
||||
}
|
||||
if(msg['type'] === 'handle-publish-done'){
|
||||
workerMap[msg['name']] = undefined;
|
||||
}
|
||||
if(msg['type'] === 'stream-request:h'){
|
||||
if(workerMap[msg['key']] !== undefined){
|
||||
workers[index].send({type: 'stream-request:h', id: msg['id'], key: msg['key'], available: true});
|
||||
}
|
||||
else {
|
||||
workers[index].send({type: 'stream-request:h', id: msg['id'], key: msg['key'], available: false});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import {parseAsYaml as parse} from "parse-yaml";
|
|||
import {readFileSync as read} from "fs";
|
||||
try {
|
||||
var localconfig: Object = parse(read('config/config.yml'));
|
||||
console.log('Config file found.');
|
||||
} catch (e) {
|
||||
console.log('No config file found. Exiting.');
|
||||
process.exit();
|
||||
|
@ -15,7 +16,7 @@ const config: Object = {
|
|||
domain: '',
|
||||
registration: false,
|
||||
email: null,
|
||||
restrictedNames: [ 'live', 'user', 'users', 'register', 'login' ],
|
||||
restrictedNames: [ 'live', 'user', 'users', 'register', 'login', 'invite' ],
|
||||
rootredirect: '/users/live',
|
||||
version: process.env.npm_package_version,
|
||||
}, localconfig['satyr']),
|
||||
|
@ -35,7 +36,10 @@ const config: Object = {
|
|||
ping: 30,
|
||||
ping_timeout: 60 }, localconfig['rtmp']),
|
||||
http: Object.assign({
|
||||
hsts: false, directory: './site', port: 8000
|
||||
hsts: false,
|
||||
directory: './site',
|
||||
port: 8000,
|
||||
server_side_render: true
|
||||
}, localconfig['http']),
|
||||
media: Object.assign({
|
||||
record: false,
|
||||
|
@ -80,6 +84,10 @@ const config: Object = {
|
|||
username: null,
|
||||
token: null
|
||||
}, localconfig['chat']['twitch'])
|
||||
}
|
||||
},
|
||||
twitch_mirror: Object.assign({
|
||||
enabled: false,
|
||||
ingest: null
|
||||
}, localconfig['twitch_mirror'])
|
||||
};
|
||||
export { config };
|
|
@ -9,6 +9,14 @@ var cryptoconfig: Object;
|
|||
function init (){
|
||||
raw = mysql.createPool(config['database']);
|
||||
cryptoconfig = config['crypto'];
|
||||
console.log('Connected to database.');
|
||||
}
|
||||
|
||||
function initRTMPCluster(){
|
||||
let cfg = config['database'];
|
||||
cfg['connectionLimit'] = Math.floor(config['database']['connectionLimit'] / require('os').cpus().length);
|
||||
raw = mysql.createPool(cfg);
|
||||
cryptoconfig = config['crypto'];
|
||||
}
|
||||
|
||||
async function addUser(name: string, password: string){
|
||||
|
@ -21,6 +29,7 @@ async function addUser(name: string, password: string){
|
|||
await query('INSERT INTO users (username, password_hash, stream_key, record_flag) VALUES ('+raw.escape(name)+', '+raw.escape(hash)+', '+raw.escape(key)+', 0)');
|
||||
await query('INSERT INTO user_meta (username, title, about, live) VALUES ('+raw.escape(name)+',\'\',\'\',false)');
|
||||
await query('INSERT INTO chat_integration (username, irc, xmpp, twitch, discord) VALUES ('+raw.escape(name)+',\'\',\'\',\'\',\'\')');
|
||||
await query('INSERT INTO twitch_mirror (username) VALUES ('+raw.escape(name)+')');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -29,6 +38,8 @@ async function rmUser(name: string){
|
|||
if(!exist[0]) return false;
|
||||
await query('delete from users where username='+raw.escape(name)+' limit 1');
|
||||
await query('delete from user_meta where username='+raw.escape(name)+' limit 1');
|
||||
await query('delete from chat_integration where username='+raw.escape(name)+' limit 1');
|
||||
await query('delete from twitch_mirror where username='+raw.escape(name)+' limit 1');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -59,4 +70,4 @@ async function hash(pwd){
|
|||
return await bcrypt.hash(pwd, cryptoconfig['saltRounds']);
|
||||
}
|
||||
|
||||
export { query, raw, init, addUser, rmUser, validatePassword, hash, genKey };
|
||||
export { query, raw, init, addUser, rmUser, validatePassword, hash, genKey, initRTMPCluster };
|
|
@ -0,0 +1,8 @@
|
|||
import * as db from "../database";
|
||||
|
||||
async function run () {
|
||||
await db.query('CREATE TABLE IF NOT EXISTS db_meta(version SMALLINT)');
|
||||
await db.query('INSERT INTO db_meta (version) VALUES (0)');
|
||||
}
|
||||
|
||||
export { run }
|
|
@ -0,0 +1,9 @@
|
|||
import * as db from "../database";
|
||||
|
||||
async function run () {
|
||||
await db.query('CREATE TABLE IF NOT EXISTS twitch_mirror(username VARCHAR(25), enabled TINYINT DEFAULT 0, twitch_key VARCHAR(50) DEFAULT \"\")');
|
||||
await db.query('INSERT INTO twitch_mirror(username) SELECT username FROM users');
|
||||
await db.query('INSERT INTO db_meta (version) VALUES (1)');
|
||||
}
|
||||
|
||||
export { run }
|
|
@ -0,0 +1,8 @@
|
|||
import * as db from "../database";
|
||||
|
||||
async function run () {
|
||||
await db.query('CREATE TABLE IF NOT EXISTS invites(code VARCHAR(150))');
|
||||
await db.query('INSERT INTO db_meta (version) VALUES (2)');
|
||||
}
|
||||
|
||||
export { run }
|
67
src/http.ts
67
src/http.ts
|
@ -12,9 +12,6 @@ import * as chatInteg from "./chat";
|
|||
import { config } from "./config";
|
||||
import { readdir, readFileSync, writeFileSync } from "fs";
|
||||
import { JWT, JWK } from "jose";
|
||||
import { strict } from "assert";
|
||||
import { parse } from "path";
|
||||
import { isBuffer } from "util";
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
@ -55,12 +52,16 @@ async function init(){
|
|||
});
|
||||
}
|
||||
app.disable('x-powered-by');
|
||||
//site handlers
|
||||
//server-side site routes
|
||||
if(config['http']['server_side_render'])
|
||||
await initSite(config['satyr']['registration']);
|
||||
//api handlers
|
||||
//api routes
|
||||
await initAPI();
|
||||
//static files if nothing else matches first
|
||||
//static files if nothing else matches
|
||||
app.use(express.static(config['http']['directory']));
|
||||
//client-side site routes
|
||||
if(!config['http']['server_side_render'])
|
||||
await initFE();
|
||||
//404 Handler
|
||||
app.use(function (req, res, next) {
|
||||
if(tryDecode(req.cookies.Authorization)) {
|
||||
|
@ -73,6 +74,21 @@ async function init(){
|
|||
server.listen(config['http']['port']);
|
||||
}
|
||||
|
||||
async function initFE(){
|
||||
app.get('/', (req, res) => {
|
||||
res.redirect(config['satyr']['rootredirect']);
|
||||
});
|
||||
app.get('/nunjucks-slim.js', (req, res) => {
|
||||
res.sendFile(process.cwd()+'/node_modules/nunjucks/browser/nunjucks-slim.js');
|
||||
});
|
||||
app.get('/chat', (req, res) => {
|
||||
res.sendFile(process.cwd()+'/templates/chat.html');
|
||||
});
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(process.cwd()+'/'+config['http']['directory']+'/index.html');
|
||||
});
|
||||
}
|
||||
|
||||
async function newNick(socket, skip?: boolean, i?: number) {
|
||||
if(socket.handshake.headers['cookie'] && !skip){
|
||||
let c = await parseCookie(socket.handshake.headers['cookie']);
|
||||
|
@ -156,7 +172,7 @@ async function initAPI() {
|
|||
ping_timeout: config['rtmp']['ping_timeout']
|
||||
},
|
||||
media: {
|
||||
vods: config['config']['media']['record'],
|
||||
vods: config['media']['record'],
|
||||
publicEndpoint: config['media']['publicEndpoint'],
|
||||
privateEndpoint: config['media']['privateEndpoint'],
|
||||
adaptive: config['transcode']['adaptive']
|
||||
|
@ -194,7 +210,7 @@ async function initAPI() {
|
|||
});
|
||||
});
|
||||
app.post('/api/users/all', (req, res) => {
|
||||
let qs = 'SELECT username,title FROM user_meta';
|
||||
let qs = 'SELECT username,title,live FROM user_meta';
|
||||
|
||||
if(req.body.sort) {
|
||||
switch (req.body.sort) {
|
||||
|
@ -224,6 +240,23 @@ async function initAPI() {
|
|||
});
|
||||
});
|
||||
app.post('/api/register', (req, res) => {
|
||||
if("invite" in req.body){
|
||||
api.validInvite(req.body.invite).then((v) => {
|
||||
if(v){
|
||||
api.register(req.body.username, req.body.password, req.body.confirm, true).then((result) => {
|
||||
if(result[0]) return genToken(req.body.username).then((t) => {
|
||||
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
|
||||
res.json(result);
|
||||
api.useInvite(req.body.invite);
|
||||
return;
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
}
|
||||
else res.json({error: "invalid invite code"});
|
||||
});
|
||||
}
|
||||
else
|
||||
api.register(req.body.username, req.body.password, req.body.confirm).then( (result) => {
|
||||
if(result[0]) return genToken(req.body.username).then((t) => {
|
||||
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
|
||||
|
@ -238,10 +271,14 @@ async function initAPI() {
|
|||
if(t) {
|
||||
if(req.body.record === "true") req.body.record = true;
|
||||
else if(req.body.record === "false") req.body.record = false;
|
||||
if(req.body.twitch === "true") req.body.twitch = true;
|
||||
else if(req.body.twitch === "false") req.body.twitch = false;
|
||||
return api.update({name: t['username'],
|
||||
title: "title" in req.body ? req.body.title : false,
|
||||
bio: "bio" in req.body ? req.body.bio : false,
|
||||
rec: "record" in req.body ? req.body.record : "NA"
|
||||
rec: "record" in req.body ? req.body.record : "NA",
|
||||
twitch: "twitch" in req.body ? req.body.twitch: "NA",
|
||||
twitch_key: "twitch_key" in req.body ? req.body.twitch_key : false
|
||||
}).then((r) => {
|
||||
res.json(r);
|
||||
return;
|
||||
|
@ -340,6 +377,7 @@ async function initAPI() {
|
|||
if(req.cookies.Authorization) validToken(req.cookies.Authorization).then((t) => {
|
||||
if(t) {
|
||||
if(t['exp'] - 86400 < Math.floor(Date.now() / 1000)){
|
||||
res.cookie('X-Auth-As', t['username'], {maxAge: 604800000, httpOnly: false, sameSite: 'Lax'});
|
||||
return genToken(t['username']).then((t) => {
|
||||
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
|
||||
res.json({success:""});
|
||||
|
@ -361,6 +399,7 @@ async function initAPI() {
|
|||
if(!result){
|
||||
genToken(req.body.username).then((t) => {
|
||||
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
|
||||
res.cookie('X-Auth-As', req.body.username, {maxAge: 604800000, httpOnly: false, sameSite: 'Lax'});
|
||||
res.json({success:""});
|
||||
})
|
||||
}
|
||||
|
@ -482,6 +521,12 @@ async function initSite(openReg) {
|
|||
}
|
||||
else res.render('login.njk',njkconf);
|
||||
});
|
||||
app.get('/invite/:code', (req, res) => {
|
||||
if(tryDecode(req.cookies.Authorization)) {
|
||||
res.redirect('/profile');
|
||||
}
|
||||
else res.render('invite.njk',Object.assign({icode: req.params.code}, njkconf));
|
||||
});
|
||||
app.get('/register', (req, res) => {
|
||||
if(tryDecode(req.cookies.Authorization) || !openReg) {
|
||||
res.redirect(njkconf.rootredirect);
|
||||
|
@ -492,7 +537,9 @@ async function initSite(openReg) {
|
|||
if(tryDecode(req.cookies.Authorization)) {
|
||||
db.query('select * from user_meta where username='+db.raw.escape(JWT.decode(req.cookies.Authorization)['username'])).then((result) => {
|
||||
db.query('select record_flag from users where username='+db.raw.escape(JWT.decode(req.cookies.Authorization)['username'])).then((r2) => {
|
||||
res.render('profile.njk', Object.assign({rflag: r2[0]}, {meta: result[0]}, {auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
|
||||
db.query('select enabled from twitch_mirror where username='+db.raw.escape(JWT.decode(req.cookies.Authorization)['username'])).then((r3) => {
|
||||
res.render('profile.njk', Object.assign({twitch: r3[0]}, {rflag: r2[0]}, {meta: result[0]}, {auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
|
||||
});
|
||||
});
|
||||
});
|
||||
//res.render('profile.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
|
||||
|
|
|
@ -4,12 +4,13 @@ import {init as initHTTP} from "./http";
|
|||
import {init as clean} from "./cleanup";
|
||||
import {init as initChat} from "./chat";
|
||||
import { config } from "./config";
|
||||
import { execFile } from "child_process";
|
||||
|
||||
async function run() {
|
||||
await initDB();
|
||||
await clean();
|
||||
await initHTTP();
|
||||
await initRTMP();
|
||||
config['rtmp']['cluster'] ? execFile(process.cwd()+'/node_modules/.bin/ts-node' [process.cwd()+'src/cluster.ts']) : await initRTMP();
|
||||
await initChat();
|
||||
console.log(`Satyr v${config['satyr']['version']} ready`);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import {init as initDB} from "./database";
|
||||
import {init as clean} from "./cleanup";
|
||||
import { config } from "./config";
|
||||
|
||||
async function run() {
|
||||
await initDB();
|
||||
await clean(false);
|
||||
}
|
||||
run().then(() => {process.exit()});
|
|
@ -68,6 +68,15 @@ function init () {
|
|||
console.log('[NodeMediaServer] Skipping recording for stream:',id);
|
||||
}
|
||||
db.query('update user_meta set live=true where username=\''+results[0].username+'\' limit 1');
|
||||
db.query('SELECT twitch_key,enabled from twitch_mirror where username='+db.raw.escape(results[0].username)+' limit 1').then(async (tm) => {
|
||||
if(!tm[0]['enabled'] || !config['twitch_mirror']['enabled'] || !config['twitch_mirror']['ingest']) return;
|
||||
console.log('[NodeMediaServer] Mirroring to twitch for stream:',id)
|
||||
execFile(config['media']['ffmpeg'], ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+config['rtmp']['port']+'/'+config['media']['privateEndpoint']+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', '-f', 'flv', config['twitch_mirror']['ingest']+tm[0]['twitch_key']], {
|
||||
detached: true,
|
||||
stdio : 'inherit',
|
||||
maxBuffer: Infinity
|
||||
}).unref();
|
||||
});
|
||||
console.log('[NodeMediaServer] Stream key ok for stream:',id);
|
||||
}
|
||||
else{
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<title>{{ sitename }}</title>
|
||||
<script>
|
||||
//should check for and refresh login tokens on pageload..
|
||||
if(document.cookie.match(/^(.*;)?\s*Authorization\s*=\s*[^;]+(.*)?$/) !== null) {
|
||||
if(document.cookie.match(/^(.*;)?\s*X-Auth-As\s*=\s*[^;]+(.*)?$/) !== null) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/api/login", true);
|
||||
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
{% extends "base.njk" %}
|
||||
{% block content %}
|
||||
<h3>You've been invited to {{ sitename }}</h3><span style="font-size: small;">Already registered? Log in <a href="/login">here</a>.</br></br></span>
|
||||
<!--<div id="jscontainer" style="height: 100%;">
|
||||
<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;">-->
|
||||
<form action="/api/register" method="POST" target="responseFrame">
|
||||
Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
|
||||
Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
|
||||
Confirm: </br><input type="password" name="confirm" style="min-width: 300px"/></br></br>
|
||||
<input type="hidden" name="invite" style="min-width: 300px" value="{{icode}}"/>
|
||||
<input type="submit" value="Submit">
|
||||
</form></br>
|
||||
|
||||
<!--</div>
|
||||
<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;">-->
|
||||
{% include "tos.html" %}</br>
|
||||
<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
|
||||
<!--</div>
|
||||
</div>-->
|
||||
{% endblock %}
|
|
@ -8,7 +8,7 @@
|
|||
{% else %}
|
||||
No recordings found!
|
||||
{% endeach %}
|
||||
<input type="submit" value="Delete">
|
||||
</br><input type="submit" value="Delete">
|
||||
</form>
|
||||
<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
|
||||
{% endblock %}
|
|
@ -5,7 +5,9 @@
|
|||
<form action="/api/user/update" method="POST" target="responseFrame" id="profile">
|
||||
Stream Title: </br><textarea form="profile" name="title" style="min-width: 320px;resize: none;font-size: large;text-align: center;" value="{{meta.title}}">{{meta.title}}</textarea></br>
|
||||
Bio: </br><textarea form="profile" name="bio" style="min-width: 320px; min-height: 150px;resize: none;font-size: inherit;" value="{{meta.about}}">{{meta.about}}</textarea></br>
|
||||
Record VODs: <input type="radio" name="record" value="true" {% if rflag.record_flag %}checked{% endif %}> Yes<input type="radio" name="record" value="false" {% if rflag.record_flag %}{% else %}checked{% endif %}/> No</br></br>
|
||||
ReStream to Twitch: <input type="radio" name="twitch" value="true" {% if twitch.enabled %}checked{% endif %}> Yes<input type="radio" name="twitch" value="false" {% if twitch.enabled %}{% else %}checked{% endif %}/> No</br>
|
||||
Record VODs: <input type="radio" name="record" value="true" {% if rflag.record_flag %}checked{% endif %}> Yes<input type="radio" name="record" value="false" {% if rflag.record_flag %}{% else %}checked{% endif %}/> No</br>
|
||||
Twitch Key: <textarea form="profile" name="twitch_key" style="max-height: 18px;min-width: 238px;resize: none;font-size: large;text-align: center;"></textarea></br></br>
|
||||
<input type="submit" value="Update Profile">
|
||||
</form></br>
|
||||
<form action="/api/user/streamkey" method="POST" target="responseFrame">
|
||||
|
|
Reference in New Issue