├── .babelrc ├── .browserslistrc ├── .editorconfig ├── .env.example ├── .env.prod ├── .eslintrc.js ├── .flowconfig ├── .github └── workflows │ ├── backend-cd.yml │ ├── backend-ci.yml │ ├── frontend-cd.yml │ └── frontend.yml ├── .gitignore ├── .gitmodules ├── .npmrc ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── .gitignore ├── CMakeLists.txt ├── Dockerfile ├── README.md ├── cmake │ ├── FindDoubleConversion.cmake │ ├── FindFolly.cmake │ ├── FindGFlags.cmake │ ├── FindGLog.cmake │ ├── FindJWT.cmake │ ├── FindJansson.cmake │ ├── FindLibiberty.cmake │ ├── FindMagic.cmake │ ├── FindSQLite.cmake │ ├── FindWangle.cmake │ ├── Findrapidjson.cmake │ ├── FinduWS.cmake │ └── LibFindMacros.cmake ├── container_test.yaml ├── deps.sh ├── docker-compose.yml ├── src │ ├── APIClient.cpp │ ├── APIClient.h │ ├── APIHTTPService.cpp │ ├── APIHTTPService.h │ ├── AdminHTTPService.cpp │ ├── AdminHTTPService.h │ ├── AngelThumpClient.cpp │ ├── AngelThumpClient.h │ ├── AuthHTTPService.cpp │ ├── AuthHTTPService.h │ ├── BannedStreams.cpp │ ├── BannedStreams.h │ ├── Channel.cpp │ ├── Channel.h │ ├── Config.cpp │ ├── Config.h │ ├── Curl.cpp │ ├── Curl.h │ ├── DB.h │ ├── HTTPRequest.cpp │ ├── HTTPRequest.h │ ├── HTTPResponseWriter.cpp │ ├── HTTPResponseWriter.h │ ├── HTTPRouter.h │ ├── HTTPService.cpp │ ├── HTTPService.h │ ├── IPRanges.cpp │ ├── IPRanges.h │ ├── JSON.h │ ├── MIMETypes.cpp │ ├── MIMETypes.h │ ├── Observer.h │ ├── ServicePoller.cpp │ ├── ServicePoller.h │ ├── Session.cpp │ ├── Session.h │ ├── SmashcastClient.cpp │ ├── SmashcastClient.h │ ├── StaticHTTPService.cpp │ ├── StaticHTTPService.h │ ├── Status.cpp │ ├── Status.h │ ├── Streams.cpp │ ├── Streams.h │ ├── Strings.cpp │ ├── Strings.h │ ├── TwitchClient.cpp │ ├── TwitchClient.h │ ├── Users.cpp │ ├── Users.h │ ├── ViewerStates.cpp │ ├── ViewerStates.h │ ├── WSService.cpp │ ├── WSService.h │ ├── YoutubeClient.cpp │ ├── YoutubeClient.h │ └── main.cpp └── tests │ ├── AngelthumpTest.cpp │ ├── CurlTest.cpp │ ├── HTTPRouterTest.cpp │ ├── IPRangesTest.cpp │ ├── SmashcastTest.cpp │ ├── StreamsTest.cpp │ ├── UsersTest.cpp │ ├── YoutubeTest.cpp │ └── users_test.env ├── config ├── scala-api-backend.conf ├── strims-backend.conf ├── strims-security-common.conf └── strims.conf ├── emote-update.sh ├── flow-typed └── npm │ ├── @babel │ └── polyfill_v7.x.x.js │ ├── bluebird_v3.x.x.js │ ├── classnames_v2.x.x.js │ ├── dotenv_v8.x.x.js │ ├── flow-bin_v0.x.x.js │ ├── history_v4.9.x.js │ ├── idx_v2.x.x.js │ ├── isomorphic-fetch_v2.x.x.js │ ├── lodash_v4.x.x.js │ ├── prop-types_v15.x.x.js │ ├── qs_v6.9.x.js │ ├── react-custom-scrollbars_v4.2.x.js │ ├── react-loadable_v5.x.x.js │ ├── react-redux_v5.x.x.js │ ├── react-router-dom_v4.x.x.js │ ├── redux_v4.x.x.js │ └── webpack_v4.x.x.js ├── package-lock.json ├── package.json ├── public ├── image │ ├── angelthump.png │ ├── beand.jpg │ ├── donger.png │ ├── favicon.ico │ ├── favicon.png │ ├── jigglymonkey.png │ ├── pepos.png │ ├── twitch.png │ └── youtube.png └── robots.txt ├── src ├── INITIAL_STATE.js ├── actions │ ├── index.js │ ├── postmessage.js │ └── websocket.js ├── client.jsx ├── components │ ├── AdvancedStreamEmbed.jsx │ ├── AdvancedStreamWarning.jsx │ ├── App.jsx │ ├── AsyncProfile.js │ ├── AsyncStream.jsx │ ├── AsyncStreams.js │ ├── Banned.jsx │ ├── Chat.jsx │ ├── ChatEmbed.jsx │ ├── Checkbox.jsx │ ├── CustomScrollbar.jsx │ ├── Error404.jsx │ ├── Footer.jsx │ ├── GitHubCommitLink.jsx │ ├── Header.jsx │ ├── HeaderForm.jsx │ ├── LazyLoadOnce.js │ ├── Loadable.js │ ├── Loading.jsx │ ├── Logout.jsx │ ├── M3u8StreamEmbed.jsx │ ├── MainLayout.jsx │ ├── NavButtonLink.jsx │ ├── PollCreate.jsx │ ├── PollResult.jsx │ ├── PollVote.jsx │ ├── Profile.jsx │ ├── Resizeable.jsx │ ├── RoutesWithChat.jsx │ ├── ServiceSelect.jsx │ ├── Stream.jsx │ ├── StreamAdminMenu.jsx │ ├── StreamEmbed.jsx │ ├── StreamThumbnail.jsx │ ├── Streams.jsx │ └── ThirdPartyWarning.jsx ├── css │ ├── AdvancedStreamWarning.scss │ ├── Footer.scss │ ├── Header.scss │ ├── MainLayout.scss │ ├── Polls.scss │ ├── Resizeable.scss │ ├── Stream.scss │ ├── StreamAdminMenu.scss │ ├── StreamThumbnail.scss │ ├── Streams.scss │ ├── colors.scss │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── main.scss ├── history.js ├── index.ejs ├── polyfills.js ├── records │ └── polls.js ├── reducers │ ├── afk.js │ ├── index.js │ ├── loading.js │ ├── polls.js │ ├── self.js │ ├── stream.js │ ├── streams.js │ └── ui.js ├── redux │ └── types.js ├── routes.jsx ├── store.js └── util │ ├── color.js │ ├── is-valid-advanced-url.js │ ├── is-vod.js │ └── supported-chats.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": [ 5 | "@babel/preset-env", 6 | "@babel/preset-flow", 7 | "@babel/preset-react" 8 | ], 9 | "plugins": [ 10 | "@babel/plugin-proposal-class-properties", 11 | "idx" 12 | ] 13 | }, 14 | "production": { 15 | "presets": [ 16 | "@babel/preset-env", 17 | "@babel/preset-flow", 18 | "@babel/preset-react" 19 | ], 20 | "plugins": [ 21 | "@babel/plugin-proposal-class-properties", 22 | "idx", 23 | ["transform-react-remove-prop-types", { 24 | "mode": "remove", 25 | "removeImport": true 26 | }] 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API=/api 2 | SCALA_API=/scala-api 3 | API_WS= 4 | 5 | # Time in milliseconds to wait before marking rustler AFK 6 | AFK_TIMEOUT=7200000 7 | 8 | # Database variables. 9 | DB_DB=overrustle 10 | DB_PATH=./overrustle.sqlite 11 | 12 | # URLs for the website's footer. 13 | DONATE_DO_URL= 14 | DONATE_LINODE_URL= 15 | DONATE_PAYPAL_URL= 16 | GITHUB_URL=https://github.com/MemeLabs/Rustla2 17 | CHAT_URL=https://chat.strims.gg 18 | 19 | CHAT2_DOMAIN= 20 | CHAT2_URL= 21 | 22 | # How often clients should cache bust thumbnails in the stream index 23 | THUMBNAIL_REFRESH_INTERVAL=60000 24 | 25 | # Randomly-generated string for signing JWTs. 26 | JWT_SECRET= 27 | 28 | # Name of the JWT cookie. 29 | JWT_NAME=jwt 30 | 31 | # JWT cookie domain 32 | JWT_DOMAIN=strims.gg 33 | 34 | # JWT cookie max age 35 | JWT_TTL=2592000 36 | 37 | # Port to run the server on. 38 | PORT=8076 39 | 40 | # How often updates of stream information should happen (in milliseconds). 41 | # Default is 60000 = 60 * 1000 = 1 minute 42 | LIVECHECK_INTERVAL=60000 43 | 44 | # Twitch client credentials. Used to request information about Twitch streams. 45 | TWITCH_CLIENT_ID= 46 | TWITCH_CLIENT_SECRET= 47 | TWITCH_REDIRECT_URI=https://example.com/ 48 | 49 | # Google API key. Used to request information about YouTube videos and streams. 50 | GOOGLE_PUBLIC_API_KEY= 51 | 52 | # Minimum character length of emotes considered in fuzzy username similarity check 53 | EMOTE_SIMILARITY_MIN_LENGTH=4 54 | 55 | # Minimum character length of emotes considered in exact substring similarity check 56 | # ex. emote username result 57 | # OK oktoberfest pass 58 | # LOL lolersk8s fail 59 | EMOTE_SUBSTRING_MIN_LENGTH=2 60 | 61 | # Shared prefix character length required for fuzzy username similarity check 62 | # ex. emote username result 63 | # 4Head 3Head pass 64 | # ApeHands ApeOut fail 65 | EMOTE_SIMILARITY_PREFIX_CHECK_SIZE=2 66 | 67 | # Minimum (exclusive) allowed Levenshtein distance between username and emote 68 | EMOTE_SIMILARITY_MIN_EDIT_DISTANCE=4 69 | 70 | # Comma delimited list of emotes 71 | EMOTES=ComfyDog,ApeHands,DumpsterFire 72 | 73 | # HTTP header the server should read client IP from 74 | IP_ADDRESS_HEADER="x-client-ip" 75 | 76 | # Interval (in milliseconds) between regeneration/retransmission of full stream list 77 | STREAM_BROADCAST_INTERVAL=60000 78 | 79 | # Debounce interval (in milliseconds) for rustler count updates 80 | RUSTLER_BROADCAST_INTERVAL=100 81 | 82 | # Path to certificate and private key if the server should use HTTPS 83 | SSL_CERT_PATH=cert.pem 84 | SSL_KEY_PATH=key.pem 85 | 86 | # Path to public html directory 87 | PUBLIC_PATH=./public 88 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | API=/api 2 | SCALA_API=/scala-api 3 | API_WS=wss://strims.gg/ws 4 | 5 | AFK_TIMEOUT=7200000 6 | 7 | # URLs for the website's footer. 8 | DONATE_DO_URL= 9 | DONATE_LINODE_URL= 10 | DONATE_PAYPAL_URL= 11 | GITHUB_URL=https://github.com/MemeLabs/Rustla2 12 | CHAT_URL=https://chat.strims.gg 13 | 14 | CHAT2_DOMAIN=test.strims.gg 15 | CHAT2_URL=https://chat2.strims.gg 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@babel/eslint-parser', 3 | env: { 4 | browser: true, 5 | commonjs: true, 6 | es6: true, 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:flowtype/recommended', 11 | 'plugin:react/recommended', 12 | ], 13 | parserOptions: { 14 | ecmaFeatures: { 15 | impliedStrict: true, 16 | experimentalObjectRestSpread: true, 17 | jsx: true, 18 | }, 19 | sourceType: 'module', 20 | }, 21 | plugins: [ 22 | 'flowtype', 23 | 'react', 24 | ], 25 | rules: { 26 | // eslint rules 27 | 'no-console': 'warn', 28 | 'no-unused-vars': 'warn', 29 | 'semi': 'warn', 30 | 31 | // eslint-plugin-flowtype rules 32 | 'flowtype/define-flow-type': 'warn', 33 | 'flowtype/require-valid-file-annotation': 'warn', 34 | 'flowtype/semi': 'warn', 35 | 'flowtype/use-flow-type': 'warn', 36 | 37 | // eslint-plugin-react rules 38 | 'react/forbid-foreign-prop-types': ['warn', { allowInPropTypes: true }], 39 | 'react/jsx-no-comment-textnodes': 'warn', 40 | 'react/jsx-no-duplicate-props': 'warn', 41 | 'react/jsx-no-target-blank': 'warn', 42 | 'react/jsx-no-undef': 'error', 43 | 'react/jsx-pascal-case': [ 44 | 'warn', 45 | { 46 | allowAllCaps: true, 47 | ignore: [], 48 | } 49 | ], 50 | 'react/jsx-uses-react': 'warn', 51 | 'react/jsx-uses-vars': 'warn', 52 | 'react/no-danger-with-children': 'warn', 53 | 'react/no-direct-mutation-state': 'warn', 54 | 'react/no-is-mounted': 'warn', 55 | 'react/no-typos': 'error', 56 | 'react/no-unescaped-entities': 'warn', 57 | 'react/prop-types': 'warn', 58 | 'react/react-in-jsx-scope': 'error', 59 | 'react/require-render-return': 'error', 60 | 'react/style-prop-object': 'warn' 61 | }, 62 | settings: { 63 | react: { 64 | version: 'detect' 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /api 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [lints] 9 | 10 | [options] 11 | module.name_mapper='^.*\/css\/.*$' -> 'css-module-flow' 12 | 13 | [strict] 14 | -------------------------------------------------------------------------------- /.github/workflows/backend-cd.yml: -------------------------------------------------------------------------------- 1 | name: Backend Continuous Deployment 2 | on: 3 | push: 4 | paths: 5 | - '.github/workflows/backend-cd.yml' 6 | - 'api/**' 7 | - '!api/.gitignore' 8 | - '!api/README.md' 9 | - '.gitmodules' 10 | branches: 11 | - 'master' 12 | workflow_dispatch: 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v1 18 | with: 19 | submodules: true 20 | 21 | - name: Docker login 22 | env: 23 | username: ${{ github.actor }} 24 | password: ${{ secrets.GITHUB_TOKEN }} 25 | run: docker login https://ghcr.io -u ${username} -p ${password} 26 | 27 | - name: Build Third Party Image 28 | env: 29 | DOCKER_BUILDKIT: 1 30 | run: | 31 | cd ./api 32 | if git diff --exit-code ../.gitmodules || git diff --exit-code ./third-party || git diff --exit-code ./Dockerfile 33 | then 34 | docker build . -t ghcr.io/memelabs/rustla2/rustla2-api:thirdPartyBase --build-arg APP_ENV=full --target full-build 35 | docker push ghcr.io/memelabs/rustla2/rustla2-api:thirdPartyBase 36 | else 37 | return 0 38 | fi 39 | 40 | - name: Build API image 41 | env: 42 | DOCKER_BUILDKIT: 1 43 | run: cd ./api && docker build . -t ghcr.io/memelabs/rustla2/rustla2-api:stable 44 | 45 | - name: Download container structure test binary 46 | uses: fnkr/github-action-git-bash@v1 47 | with: 48 | args: "wget -O ./api/container-structure-test https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64" 49 | 50 | - name: Chmod 51 | uses: fnkr/github-action-git-bash@v1 52 | with: 53 | args: "chmod +x ./api/container-structure-test" 54 | 55 | - name: Test image 56 | uses: fnkr/github-action-git-bash@v1 57 | with: 58 | args: "./api/container-structure-test test --image ghcr.io/memelabs/rustla2/rustla2-api:stable --config ./api/container_test.yaml" 59 | 60 | - name: Docker push 61 | run: docker push ghcr.io/memelabs/rustla2/rustla2-api:stable 62 | 63 | - name: ssh-deploy for rustla backend 64 | uses: appleboy/ssh-action@122f35dca5c7a216463c504741deb0de5b301953 65 | with: 66 | host: ${{ secrets.HOST }} 67 | username: ${{ secrets.USERNAME }} 68 | key: ${{ secrets.KEY }} 69 | script: | 70 | ./hooks/rustla2-api.sh 71 | -------------------------------------------------------------------------------- /.github/workflows/backend-ci.yml: -------------------------------------------------------------------------------- 1 | name: Backend Continuous Integration 2 | on: 3 | pull_request: 4 | paths: 5 | - '.github/workflows/backend-ci.yml' 6 | - 'api/**' 7 | - '!api/.gitignore' 8 | - '!api/README.md' 9 | - '.gitmodules' 10 | push: 11 | paths: 12 | - '.github/workflows/backend-ci.yml' 13 | - 'api/**' 14 | - '!api/.gitignore' 15 | - '!api/README.md' 16 | - '.gitmodules' 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v1 22 | with: 23 | submodules: true 24 | 25 | - name: Docker login 26 | env: 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | run: docker login https://docker.pkg.github.com -u ${username} -p ${password} 30 | 31 | - name: Build Third Party Image 32 | env: 33 | DOCKER_BUILDKIT: 1 34 | run: | 35 | cd ./api 36 | if git diff --exit-code ../.gitmodules || git diff --exit-code ./third-party || git diff --exit-code ./Dockerfile 37 | then 38 | docker build . -t docker.pkg.github.com/memelabs/rustla2/rustla2-api:thirdPartyBase --build-arg APP_ENV=full --target full-build 39 | else 40 | return 0 41 | fi 42 | 43 | - name: Build API Image 44 | env: 45 | DOCKER_BUILDKIT: 1 46 | run: cd ./api && docker build . -t docker.pkg.github.com/memelabs/rustla2/rustla2-api:stable 47 | 48 | - name: Download container structure test binary 49 | uses: fnkr/github-action-git-bash@v1 50 | with: 51 | args: "wget -O ./api/container-structure-test https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64" 52 | 53 | - name: Chmod 54 | uses: fnkr/github-action-git-bash@v1 55 | with: 56 | args: "chmod +x ./api/container-structure-test" 57 | 58 | - name: Test image 59 | uses: fnkr/github-action-git-bash@v1 60 | with: 61 | args: "./api/container-structure-test test --image docker.pkg.github.com/memelabs/rustla2/rustla2-api:stable --config ./api/container_test.yaml" 62 | -------------------------------------------------------------------------------- /.github/workflows/frontend-cd.yml: -------------------------------------------------------------------------------- 1 | name: Frontend Continuous Deployment 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | submodules: true 17 | - name: Docker login 18 | env: 19 | username: ${{ github.actor }} 20 | password: ${{ secrets.GITHUB_TOKEN }} 21 | run: docker login https://ghcr.io -u ${username} -p ${password} 22 | - name: Build image 23 | run: docker build . -t ghcr.io/memelabs/rustla2/rustla2-ui:latest 24 | - name: Publish image 25 | run: docker push ghcr.io/memelabs/rustla2/rustla2-ui:latest 26 | - name: ssh-deploy for rustla ui 27 | uses: appleboy/ssh-action@122f35dca5c7a216463c504741deb0de5b301953 28 | with: 29 | host: ${{ secrets.HOST }} 30 | username: ${{ secrets.USERNAME }} 31 | key: ${{ secrets.KEY }} 32 | script: | 33 | ./hooks/rustla2-ui.sh 34 | - name: Purge cache 35 | run: | 36 | curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${{ secrets.CF_RUSTLA2_ZONE }}/purge_cache" \ 37 | -H "Authorization: Bearer ${{ secrets.CF_TOKEN }}" \ 38 | -H "Content-Type:application/json" \ 39 | --data '{"purge_everything": true}' 40 | -------------------------------------------------------------------------------- /.github/workflows/frontend.yml: -------------------------------------------------------------------------------- 1 | name: Frontend build 2 | on: [pull_request, push] 3 | jobs: 4 | build: 5 | name: Node.js v${{ matrix.node-version }}, ${{ matrix.os }} 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | node-version: [12, 14] 10 | os: [ubuntu-latest, windows-latest, macOS-latest] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Setup Node.js v${{ matrix.node-version }} 14 | uses: actions/setup-node@v2.5.0 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - name: Update npm 18 | run: | 19 | npm install -g npm@8.15.0 20 | npm --version 21 | - name: Install dependencies 22 | uses: bahmutov/npm-install@v1.7.8 23 | - name: Build 24 | run: npm run build:production 25 | - name: Lint 26 | run: npm run lint 27 | - name: Flow 28 | run: npm run flow 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | public/assets/* 4 | public/index.html 5 | .env 6 | *.log 7 | *.sqlite 8 | .vscode 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "api/third-party/folly"] 2 | path = api/third-party/folly 3 | url = https://github.com/facebook/folly.git 4 | [submodule "api/third-party/googletest"] 5 | path = api/third-party/googletest 6 | url = https://github.com/google/googletest.git 7 | [submodule "api/third-party/jansson"] 8 | path = api/third-party/jansson 9 | url = https://github.com/akheron/jansson.git 10 | [submodule "api/third-party/jwt-cpp"] 11 | path = api/third-party/jwt-cpp 12 | url = https://github.com/pokowaka/jwt-cpp.git 13 | [submodule "api/third-party/rapidjson"] 14 | path = api/third-party/rapidjson 15 | url = https://github.com/Tencent/rapidjson.git 16 | [submodule "api/third-party/sqlite_modern_cpp"] 17 | path = api/third-party/sqlite_modern_cpp 18 | url = https://github.com/aminroosta/sqlite_modern_cpp.git 19 | [submodule "api/third-party/uWebSockets"] 20 | path = api/third-party/uWebSockets 21 | url = https://github.com/uNetworking/uWebSockets.git 22 | [submodule "api/third-party/wangle"] 23 | path = api/third-party/wangle 24 | url = https://github.com/facebook/wangle.git 25 | [submodule "api/third-party/openssl"] 26 | path = api/third-party/openssl 27 | url = https://github.com/openssl/openssl.git 28 | [submodule "api/third-party/fizz"] 29 | path = api/third-party/fizz 30 | url = https://github.com/facebookincubator/fizz.git 31 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.22.8-alpine AS build 2 | 3 | RUN mkdir /ui 4 | WORKDIR /ui 5 | 6 | COPY public ./public 7 | COPY src ./src 8 | COPY \ 9 | .babelrc \ 10 | .eslintrc.js \ 11 | .flowconfig \ 12 | package-lock.json \ 13 | package.json \ 14 | webpack.config.js \ 15 | ./ 16 | 17 | ENV ENV_SRC=".env.prod" 18 | COPY ${ENV_SRC} .env 19 | 20 | RUN npm install 21 | RUN npm run build:production 22 | 23 | FROM nginx:stable-alpine 24 | 25 | COPY --from=build /ui/public /usr/share/nginx/html/ 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ILiedAboutCake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # strims.gg 2 | 3 | Livestream viewing with `strims.gg` chat. 4 | 5 | ## Setup 6 | 7 | First, ensure that you have Node.js (version 12 or greater) and `npm` 8 | (preferably the latest stable release) installed. Then, 9 | 10 | ``` bash 11 | $ git clone https://github.com/MemeLabs/Rustla2.git 12 | $ cd Rustla2/ 13 | $ cp .env.example .env 14 | ``` 15 | 16 | Edit `.env` to change various environment variables. Most importantly, 17 | `JWT_SECRET` should **not** be left blank. The following is recommended: 18 | 19 | ```bash 20 | $ sed -i "s/JWT_SECRET=/JWT_SECRET=$(head -c 22 /dev/urandom | base64 | tr -dc A-Za-z0-9)/" .env 21 | ``` 22 | 23 | The command is a little different on macOS: 24 | 25 | ```bash 26 | $ sed -i "" "s/JWT_SECRET=/JWT_SECRET=$(head -c 22 /dev/urandom | base64 | tr -dc A-Za-z0-9)/" .env 27 | ``` 28 | 29 | Install dependencies and build the frontend: 30 | 31 | ``` bash 32 | $ npm ci 33 | $ npm run build 34 | ``` 35 | 36 | Then, follow the instructions in `api/README.md` for how to start the backend 37 | (which includes the API server and a web server for the frontend). 38 | 39 | ### Creating Twitch client 40 | 41 | Retrieving thumbnails, viewer counts, and live statuses for Twitch streams 42 | requires a registered Twitch client. 43 | 44 | 1. Go to 45 | 2. Name the application whatever you want. The important part is that the 46 | **Redirect URI** is set to `$API/oauth`. For example: 47 | 48 | ![](https://i.imgur.com/hy4ii2c.png) 49 | 3. Edit `.env` to include your **Redirect URI**, **Client ID**, and **Client 50 | Secret**: 51 | 52 | ``` 53 | TWITCH_CLIENT_ID=yourclientid 54 | TWITCH_CLIENT_SECRET=yourclientsecret 55 | TWITCH_REDIRECT_URI=http://localhost:3000/oauth 56 | ``` 57 | 58 | ## UI dev environment setup 59 | 60 | Install the latest stable version of Node. 61 | 62 | ```bash 63 | $ git clone https://github.com/MemeLabs/Rustla2.git 64 | $ cd Rustla2/ 65 | $ cp .env.example .env 66 | ``` 67 | 68 | Update `API_WS` in the config with the production WebSocket API URL. 69 | 70 | ``` 71 | API_WS=wss://strims.gg/ws 72 | ``` 73 | 74 | Install the dependencies and start the webpack dev server 75 | 76 | ```bash 77 | $ npm ci 78 | $ npm run dev-server 79 | ``` 80 | 81 | You can access the dev server from your browser at `http://localhost:3000`. 82 | 83 | ## Building for production 84 | 85 | The process of building for development is essentially the same as building for 86 | production, except with the additional step of minifying the frontend JavaScript 87 | code. This reduces the overall size of the bundle that is served to users, which 88 | can result in faster page loads. 89 | 90 | ``` bash 91 | npm run build:production 92 | ``` 93 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG APP_ENV=partial 2 | 3 | FROM ghcr.io/memelabs/rustla2/rustla2-api:thirdPartyBase as partial-build 4 | 5 | FROM ubuntu:16.04 as full-build 6 | 7 | RUN apt-get update && apt-get install -y \ 8 | automake \ 9 | autoconf \ 10 | autoconf-archive \ 11 | binutils-dev \ 12 | build-essential \ 13 | cmake \ 14 | git-core \ 15 | g++ \ 16 | libboost-all-dev \ 17 | libcurl4-gnutls-dev \ 18 | libevent-dev \ 19 | libdouble-conversion-dev \ 20 | libgoogle-glog-dev \ 21 | libgflags-dev \ 22 | libiberty-dev \ 23 | libjemalloc-dev \ 24 | liblz4-dev \ 25 | liblzma-dev \ 26 | libmagic-dev \ 27 | libsnappy-dev \ 28 | libsodium-dev \ 29 | libssl-dev \ 30 | libsqlite3-dev \ 31 | libtool \ 32 | make \ 33 | pkg-config \ 34 | python \ 35 | sqlite3 \ 36 | zlib1g-dev && \ 37 | rm -rf /var/lib/apt/lists/* 38 | 39 | WORKDIR /api 40 | COPY third-party ./third-party 41 | COPY deps.sh . 42 | 43 | RUN bash deps.sh 44 | 45 | FROM ${APP_ENV}-build as base 46 | 47 | COPY cmake ./cmake 48 | COPY src ./src 49 | COPY tests ./tests 50 | COPY CMakeLists.txt . 51 | 52 | WORKDIR /api/build 53 | RUN cmake .. \ 54 | && make rustla2_api 55 | 56 | FROM ubuntu:16.04 57 | 58 | RUN useradd -m rustla \ 59 | && apt-get update \ 60 | && apt-get install -y libmagic-dev curl \ 61 | && rm -rf /var/lib/apt/lists/* 62 | 63 | COPY --from=base /api/build/rustla2_api /api/ 64 | 65 | COPY --from=base \ 66 | /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 \ 67 | /lib/x86_64-linux-gnu/libgcc_s.so.1 \ 68 | /lib/x86_64-linux-gnu/libkeyutils.so.1 \ 69 | /lib/x86_64-linux-gnu/libssl.so.1.0.0 \ 70 | /lib/x86_64-linux-gnu/ 71 | 72 | COPY --from=base /usr/lib/libuWS.so /usr/lib/ 73 | 74 | COPY --from=base \ 75 | /usr/lib/x86_64-linux-gnu/libasn1.so.8 \ 76 | /usr/lib/x86_64-linux-gnu/libcurl-gnutls.so.4 \ 77 | /usr/lib/x86_64-linux-gnu/libdouble-conversion.so.1 \ 78 | /usr/lib/x86_64-linux-gnu/libffi.so.6 \ 79 | /usr/lib/x86_64-linux-gnu/libgflags.so.2 \ 80 | /usr/lib/x86_64-linux-gnu/libglog.so.0 \ 81 | /usr/lib/x86_64-linux-gnu/libgmp.so.10 \ 82 | /usr/lib/x86_64-linux-gnu/libgnutls.so.30 \ 83 | /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2 \ 84 | /usr/lib/x86_64-linux-gnu/libgssapi.so.3 \ 85 | /usr/lib/x86_64-linux-gnu/libhcrypto.so.4 \ 86 | /usr/lib/x86_64-linux-gnu/libheimbase.so.1 \ 87 | /usr/lib/x86_64-linux-gnu/libheimntlm.so.0 \ 88 | /usr/lib/x86_64-linux-gnu/libhogweed.so.4 \ 89 | /usr/lib/x86_64-linux-gnu/libhx509.so.5 \ 90 | /usr/lib/x86_64-linux-gnu/libidn.so.11 \ 91 | /usr/lib/x86_64-linux-gnu/libk5crypto.so.3 \ 92 | /usr/lib/x86_64-linux-gnu/libkrb5.so.26 \ 93 | /usr/lib/x86_64-linux-gnu/libkrb5.so.3 \ 94 | /usr/lib/x86_64-linux-gnu/libkrb5support.so.0 \ 95 | /usr/lib/x86_64-linux-gnu/liblber-2.4.so.2 \ 96 | /usr/lib/x86_64-linux-gnu/libldap_r-2.4.so.2 \ 97 | /usr/lib/x86_64-linux-gnu/libnettle.so.6 \ 98 | /usr/lib/x86_64-linux-gnu/libp11-kit.so.0 \ 99 | /usr/lib/x86_64-linux-gnu/libroken.so.18 \ 100 | /usr/lib/x86_64-linux-gnu/librtmp.so.1 \ 101 | /usr/lib/x86_64-linux-gnu/libsasl2.so.2 \ 102 | /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 \ 103 | /usr/lib/x86_64-linux-gnu/libtasn1.so.6 \ 104 | /usr/lib/x86_64-linux-gnu/libunwind.so.8 \ 105 | /usr/lib/x86_64-linux-gnu/libwind.so.0 \ 106 | /usr/lib/x86_64-linux-gnu/ 107 | 108 | WORKDIR /api 109 | USER rustla 110 | 111 | ENV extraArgs "" 112 | ENTRYPOINT ["/bin/bash", "-c", "/api/rustla2_api ${extraArgs} && sleep infinity"] 113 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | ## Setup 2 | 3 | 1. Build the static assets and create a config () 4 | 2. Install Docker () 5 | 3. Build the Docker container 6 | ``` 7 | $ git submodule update --init 8 | $ docker build . -t rustla2-api 9 | ``` 10 | 4. Set permissions 11 | ``` 12 | $ sudo chown 1000:1000 ~/Rustla2 13 | ``` 14 | The user inside the container is mapped to 1000:1000, when mounting ~/Rustla2 onto the container it needs the same permissions to be able to write to the directory. 15 | 16 | 17 | 5. Start the container 18 | ``` 19 | $ docker run -d --name rustla2 -p 8076:8076 -v ~/Rustla2:/Rustla2:rw -w /Rustla2 rustla2-api:latest 20 | ``` 21 | If the first build fails, check your CPU and RAM usage. The process requires a fair amount of resources. Try setting the environment variable ``JOBS=1`` as explained in ``deps.sh`` and restart the process. 22 | 23 | 4. Test if everything worked by running 24 | ``` 25 | $ curl -v http://localhost:8076/api 26 | ``` 27 | 28 | ### Manual account setup 29 | 30 | It might be desirable for administrators or developers to create user accounts 31 | without the need to go through the Twitch OAuth setup. 32 | 33 | 1. Create a new account in the database (the database file will be created 34 | after first running the server): 35 | ``` 36 | $ sqlite3 ./overrustle.sqlite 37 | sqlite> INSERT INTO "users" VALUES('13374242-1337-1337-1337-cccccccccccc',1337,'testuser','testuser','','twitch','admin','','2018-04-16 19:53:02',0,0,'','2018-04-01 20:00:00','2018-04-11 20:00:00',0,0,0); 38 | ``` 39 | 2. Forge the correct jwt cookie to be able to access this account: 40 | ``` 41 | $ node 42 | > var jwt = require('jwt-simple'); 43 | > jwt.encode({'id': '13374242-1337-1337-1337-cccccccccccc', 'exp':1999999999}, JWT_SECRET); 44 | ``` 45 | 46 | 3. In your browser create a cookie for your Rustla2 domain (possibly 47 | `localhost`) named `jwt` (or whatever you set `JWT_NAME` to be), and set 48 | the value of it to the output of the above. Depending on your browser, 49 | you might need to install some addon for this. 50 | 51 | 4. Reload the page. You should be logged in, which can be seen at the top 52 | right of the page. 53 | 54 | -------------------------------------------------------------------------------- /api/cmake/FindDoubleConversion.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find libdouble-conversion. 2 | # 3 | # This module defines: 4 | # DOUBLE_CONVERSION_INCLUDE_DIR 5 | # DOUBLE_CONVERSION_LIBRARY 6 | 7 | find_path(DOUBLE_CONVERSION_INCLUDE_DIR 8 | NAMES 9 | double-conversion.h 10 | PATHS 11 | /usr/include/double-conversion 12 | /usr/local/include/double-conversion) 13 | find_library(DOUBLE_CONVERSION_LIBRARY NAMES double-conversion) 14 | 15 | include(FindPackageHandleStandardArgs) 16 | FIND_PACKAGE_HANDLE_STANDARD_ARGS( 17 | DOUBLE_CONVERSION DEFAULT_MSG 18 | DOUBLE_CONVERSION_LIBRARY DOUBLE_CONVERSION_INCLUDE_DIR) 19 | 20 | if (NOT DOUBLE_CONVERSION_FOUND) 21 | message(STATUS "Using third-party bundled double-conversion") 22 | else() 23 | message(STATUS "Found double-conversion: ${DOUBLE_CONVERSION_LIBRARY}") 24 | endif (NOT DOUBLE_CONVERSION_FOUND) 25 | 26 | mark_as_advanced(DOUBLE_CONVERSION_INCLUDE_DIR DOUBLE_CONVERSION_LIBRARY) 27 | -------------------------------------------------------------------------------- /api/cmake/FindFolly.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find Facebook folly library 2 | # This will define 3 | # FOLLY_FOUND 4 | # FOLLY_INCLUDE_DIR 5 | # FOLLY_LIBRARIES 6 | 7 | find_package(DoubleConversion REQUIRED) 8 | 9 | find_path( 10 | FOLLY_INCLUDE_DIR 11 | NAMES "folly/String.h" 12 | HINTS 13 | "/usr/local/include" 14 | ) 15 | 16 | find_library( 17 | FOLLY_LIBRARY 18 | NAMES folly 19 | HINTS 20 | "/usr/local/lib" 21 | ) 22 | 23 | set(FOLLY_LIBRARIES ${FOLLY_LIBRARY} ${DOUBLE_CONVERSION_LIBRARY}) 24 | 25 | include(FindPackageHandleStandardArgs) 26 | find_package_handle_standard_args( 27 | FOLLY DEFAULT_MSG FOLLY_INCLUDE_DIR FOLLY_LIBRARIES) 28 | 29 | mark_as_advanced(FOLLY_INCLUDE_DIR FOLLY_LIBRARIES FOLLY_FOUND) 30 | 31 | if(FOLLY_FOUND AND NOT FOLLY_FIND_QUIETLY) 32 | message(STATUS "FOLLY: ${FOLLY_INCLUDE_DIR}") 33 | endif() 34 | -------------------------------------------------------------------------------- /api/cmake/FindGFlags.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find GFLAGS 2 | # 3 | # The following variables are optionally searched for defaults 4 | # GFLAGS_ROOT_DIR: Base directory where all GFLAGS components are found 5 | # 6 | # The following are set after configuration is done: 7 | # GFLAGS_FOUND 8 | # GFLAGS_INCLUDE_DIRS 9 | # GFLAGS_LIBRARIES 10 | # GFLAGS_LIBRARYRARY_DIRS 11 | 12 | include(FindPackageHandleStandardArgs) 13 | 14 | set(GFLAGS_ROOT_DIR "" CACHE PATH "Folder contains Gflags") 15 | 16 | # We are testing only a couple of files in the include directories 17 | if(WIN32) 18 | find_path(GFLAGS_INCLUDE_DIR gflags/gflags.h 19 | PATHS ${GFLAGS_ROOT_DIR}/src/windows) 20 | else() 21 | find_path(GFLAGS_INCLUDE_DIR gflags/gflags.h 22 | PATHS ${GFLAGS_ROOT_DIR}) 23 | endif() 24 | 25 | if(MSVC) 26 | find_library(GFLAGS_LIBRARY_RELEASE 27 | NAMES libgflags 28 | PATHS ${GFLAGS_ROOT_DIR} 29 | PATH_SUFFIXES Release) 30 | 31 | find_library(GFLAGS_LIBRARY_DEBUG 32 | NAMES libgflags-debug 33 | PATHS ${GFLAGS_ROOT_DIR} 34 | PATH_SUFFIXES Debug) 35 | 36 | set(GFLAGS_LIBRARY optimized ${GFLAGS_LIBRARY_RELEASE} debug ${GFLAGS_LIBRARY_DEBUG}) 37 | else() 38 | find_library(GFLAGS_LIBRARY gflags) 39 | endif() 40 | 41 | find_package_handle_standard_args(GFlags DEFAULT_MSG GFLAGS_INCLUDE_DIR GFLAGS_LIBRARY) 42 | 43 | 44 | if(GFLAGS_FOUND) 45 | set(GFLAGS_INCLUDE_DIRS ${GFLAGS_INCLUDE_DIR}) 46 | set(GFLAGS_LIBRARIES ${GFLAGS_LIBRARY}) 47 | message(STATUS "Found gflags (include: ${GFLAGS_INCLUDE_DIR}, library: ${GFLAGS_LIBRARY})") 48 | mark_as_advanced(GFLAGS_LIBRARY_DEBUG GFLAGS_LIBRARY_RELEASE 49 | GFLAGS_LIBRARY GFLAGS_INCLUDE_DIR GFLAGS_ROOT_DIR) 50 | endif() 51 | -------------------------------------------------------------------------------- /api/cmake/FindGLog.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find Glog 2 | # 3 | # The following variables are optionally searched for defaults 4 | # GLOG_ROOT_DIR: Base directory where all GLOG components are found 5 | # 6 | # The following are set after configuration is done: 7 | # GLOG_FOUND 8 | # GLOG_INCLUDE_DIRS 9 | # GLOG_LIBRARIES 10 | # GLOG_LIBRARYRARY_DIRS 11 | 12 | include(FindPackageHandleStandardArgs) 13 | 14 | set(GLOG_ROOT_DIR "" CACHE PATH "Folder contains Google glog") 15 | 16 | if(WIN32) 17 | find_path(GLOG_INCLUDE_DIR glog/logging.h 18 | PATHS ${GLOG_ROOT_DIR}/src/windows) 19 | else() 20 | find_path(GLOG_INCLUDE_DIR glog/logging.h 21 | PATHS ${GLOG_ROOT_DIR}) 22 | endif() 23 | 24 | if(MSVC) 25 | find_library(GLOG_LIBRARY_RELEASE libglog_static 26 | PATHS ${GLOG_ROOT_DIR} 27 | PATH_SUFFIXES Release) 28 | 29 | find_library(GLOG_LIBRARY_DEBUG libglog_static 30 | PATHS ${GLOG_ROOT_DIR} 31 | PATH_SUFFIXES Debug) 32 | 33 | set(GLOG_LIBRARY optimized ${GLOG_LIBRARY_RELEASE} debug ${GLOG_LIBRARY_DEBUG}) 34 | else() 35 | find_library(GLOG_LIBRARY glog 36 | PATHS ${GLOG_ROOT_DIR} 37 | PATH_SUFFIXES lib lib64) 38 | endif() 39 | 40 | find_package_handle_standard_args(Glog DEFAULT_MSG GLOG_INCLUDE_DIR GLOG_LIBRARY) 41 | 42 | if(GLOG_FOUND) 43 | set(GLOG_INCLUDE_DIRS ${GLOG_INCLUDE_DIR}) 44 | set(GLOG_LIBRARIES ${GLOG_LIBRARY}) 45 | message(STATUS "Found glog (include: ${GLOG_INCLUDE_DIR}, library: ${GLOG_LIBRARY})") 46 | mark_as_advanced(GLOG_ROOT_DIR GLOG_LIBRARY_RELEASE GLOG_LIBRARY_DEBUG 47 | GLOG_LIBRARY GLOG_INCLUDE_DIR) 48 | endif() 49 | -------------------------------------------------------------------------------- /api/cmake/FindJWT.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find JWT 2 | # Once done this will define 3 | # JWT_FOUND - System has JWT 4 | # JWT_INCLUDE_DIRS - The JWT include directories 5 | # JWT_LIBRARIES - The libraries needed to use JWT 6 | # JWT_DEFINITIONS - Compiler switches required for using JWT 7 | 8 | set(JWT_SEARCH_PATH "${JWT_SOURCE_DIR}" "${CMAKE_SOURCE_DIR}/lib/jwt-cpp") 9 | 10 | find_package(PkgConfig) 11 | pkg_check_modules(PC_JWT QUIET jwt) 12 | set(JWT_DEFINITIONS ${PC_JWT_CFLAGS_OTHER}) 13 | 14 | find_path(JWT_INCLUDE_DIR jwt/jwt_all.h 15 | HINTS ${PC_JWT_INCLUDEDIR} ${PC_JWT_INCLUDE_DIRS} 16 | PATH_SUFFIXES jwt) 17 | 18 | find_library(JWT_LIBRARY NAMES jwt 19 | HINTS ${PC_JWT_LIBDIR} ${PC_JWT_LIBRARY_DIRS}) 20 | 21 | include(FindPackageHandleStandardArgs) 22 | # handle the QUIETLY and REQUIRED arguments and set JWT_FOUND to TRUE 23 | # if all listed variables are TRUE 24 | find_package_handle_standard_args(JWT DEFAULT_MSG 25 | JWT_LIBRARY JWT_INCLUDE_DIR) 26 | 27 | mark_as_advanced(JWT_INCLUDE_DIR JWT_LIBRARY) 28 | 29 | set(JWT_LIBRARIES ${JWT_LIBRARY}) 30 | set(JWT_INCLUDE_DIRS ${JWT_INCLUDE_DIR}) 31 | -------------------------------------------------------------------------------- /api/cmake/FindJansson.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find Jansson 2 | # Once done this will define 3 | # 4 | # JANSSON_FOUND - system has Jansson 5 | # JANSSON_INCLUDE_DIRS - the Jansson include directory 6 | # JANSSON_LIBRARIES - Link these to use Jansson 7 | # 8 | # Copyright (c) 2011 Lee Hambley 9 | # 10 | # Redistribution and use is allowed according to the terms of the New 11 | # BSD license. 12 | # For details see the accompanying COPYING-CMAKE-SCRIPTS file. 13 | # 14 | 15 | if (JANSSON_LIBRARIES AND JANSSON_INCLUDE_DIRS) 16 | # in cache already 17 | set(JANSSON_FOUND TRUE) 18 | else (JANSSON_LIBRARIES AND JANSSON_INCLUDE_DIRS) 19 | find_path(JANSSON_INCLUDE_DIR 20 | NAMES 21 | jansson.h 22 | PATHS 23 | /usr/include 24 | /usr/local/include 25 | /opt/local/include 26 | /sw/include 27 | ) 28 | 29 | find_library(JANSSON_LIBRARY 30 | NAMES 31 | jansson 32 | PATHS 33 | /usr/lib 34 | /usr/local/lib 35 | /opt/local/lib 36 | /sw/lib 37 | ) 38 | 39 | set(JANSSON_INCLUDE_DIRS 40 | ${JANSSON_INCLUDE_DIR} 41 | ) 42 | 43 | if (JANSSON_LIBRARY) 44 | set(JANSSON_LIBRARIES 45 | ${JANSSON_LIBRARIES} 46 | ${JANSSON_LIBRARY} 47 | ) 48 | endif (JANSSON_LIBRARY) 49 | 50 | include(FindPackageHandleStandardArgs) 51 | find_package_handle_standard_args(Jansson DEFAULT_MSG 52 | JANSSON_LIBRARIES JANSSON_INCLUDE_DIRS) 53 | 54 | # show the JANSSON_INCLUDE_DIRS and JANSSON_LIBRARIES variables only in the advanced view 55 | mark_as_advanced(JANSSON_INCLUDE_DIRS JANSSON_LIBRARIES) 56 | 57 | endif (JANSSON_LIBRARIES AND JANSSON_INCLUDE_DIRS) 58 | -------------------------------------------------------------------------------- /api/cmake/FindLibiberty.cmake: -------------------------------------------------------------------------------- 1 | find_path(LIBIBERTY_INCLUDE_DIR NAMES libiberty.h PATH_SUFFIXES libiberty) 2 | mark_as_advanced(LIBIBERTY_INCLUDE_DIR) 3 | 4 | find_library(LIBIBERTY_LIBRARY NAMES iberty) 5 | mark_as_advanced(LIBIBERTY_LIBRARY) 6 | 7 | include(FindPackageHandleStandardArgs) 8 | FIND_PACKAGE_HANDLE_STANDARD_ARGS( 9 | LIBIBERTY 10 | REQUIRED_VARS LIBIBERTY_LIBRARY LIBIBERTY_INCLUDE_DIR) 11 | 12 | if(LIBIBERTY_FOUND) 13 | set(LIBIBERTY_LIBRARIES ${LIBIBERTY_LIBRARY}) 14 | set(LIBIBERTY_INCLUDE_DIRS ${LIBIBERTY_INCLUDE_DIR}) 15 | endif() 16 | -------------------------------------------------------------------------------- /api/cmake/FindMagic.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find libmagic 5.07 2 | # Once done this will define 3 | # 4 | # MAGIC_FOUND - system has libmagic 5 | # MAGIC_INCLUDE_DIRS - the libmagic include directory 6 | # MAGIC_LIBRARIES - Link these to use libmagic 7 | 8 | if (MAGIC_LIBRARIES AND MAGIC_INCLUDE_DIRS) 9 | # in cache already 10 | set (MAGIC_FOUND TRUE) 11 | else () 12 | 13 | find_path (MAGIC_INCLUDE_DIR 14 | NAMES 15 | magic.h 16 | PATHS 17 | /usr/include 18 | /usr/local/include 19 | /opt/local/include 20 | /sw/include 21 | ) 22 | 23 | find_library (MAGIC_LIBRARY 24 | NAMES 25 | magic 26 | PATHS 27 | /usr/lib 28 | /usr/local/lib 29 | /opt/local/lib 30 | /sw/lib 31 | ) 32 | 33 | set (MAGIC_INCLUDE_DIRS 34 | ${MAGIC_INCLUDE_DIR} 35 | ) 36 | set (MAGIC_LIBRARIES 37 | ${MAGIC_LIBRARY} 38 | ) 39 | 40 | if (MAGIC_INCLUDE_DIRS AND MAGIC_LIBRARIES) 41 | set (MAGIC_FOUND TRUE) 42 | endif () 43 | 44 | if (MAGIC_FOUND) 45 | if (NOT MAGIC_FIND_QUIETLY) 46 | message (STATUS "Found libmagic: ${MAGIC_LIBRARIES}") 47 | endif () 48 | else () 49 | if (MAGIC_FIND_REQUIRED) 50 | message (FATAL_ERROR "Could not find libmagic") 51 | endif () 52 | endif () 53 | 54 | # show the MAGIC_INCLUDE_DIRS and MAGIC_LIBRARIES variables only in the advanced view 55 | mark_as_advanced (MAGIC_INCLUDE_DIRS MAGIC_LIBRARIES) 56 | 57 | endif () 58 | -------------------------------------------------------------------------------- /api/cmake/FindSQLite.cmake: -------------------------------------------------------------------------------- 1 | # - Find include and libraries for SQLITE library 2 | # SQLITE_INCLUDE Directories to include to use SQLITE 3 | # SQLITE_INCLUDE-I Directories to include to use SQLITE (with -I) 4 | # SQLITE_LIBRARIES Libraries to link against to use SQLITE 5 | # SQLITE_FOUND SQLITE was found 6 | 7 | IF (UNIX) 8 | INCLUDE (UsePkgConfig) 9 | PKGCONFIG (sqlite3 SQLite_include_dir SQLite_link_dir SQLite_libraries SQLite_include) 10 | IF (SQLite_libraries) 11 | SET (SQLITE_FOUND TRUE) 12 | SET (SQLITE_LIBRARIES ${SQLite_libraries}) 13 | ELSE (SQLite_libraries) 14 | SET (SQLITE_FOUND FALSE) 15 | ENDIF (SQLite_libraries) 16 | ENDIF (UNIX) 17 | -------------------------------------------------------------------------------- /api/cmake/FindWangle.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2015, Yeolar 3 | # 4 | # - Try to find wangle 5 | # This will define 6 | # WANGLE_FOUND 7 | # WANGLE_INCLUDE_DIR 8 | # WANGLE_LIBRARIES 9 | 10 | cmake_minimum_required(VERSION 2.8.8) 11 | 12 | include(FindPackageHandleStandardArgs) 13 | 14 | find_library(WANGLE_LIBRARY wangle PATHS ${WANGLE_LIBRARYDIR}) 15 | find_path(WANGLE_INCLUDE_DIR "wangle/acceptor/Acceptor.h" PATHS ${WANGLE_INCLUDEDIR}) 16 | 17 | set(WANGLE_LIBRARIES ${WANGLE_LIBRARY}) 18 | 19 | find_package_handle_standard_args(Wangle 20 | REQUIRED_ARGS WANGLE_INCLUDE_DIR WANGLE_LIBRARIES) 21 | -------------------------------------------------------------------------------- /api/cmake/Findrapidjson.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find rapidjson 2 | # This will define 3 | # RAPIDJSON_FOUND 4 | # RAPIDJSON_INCLUDE_DIR 5 | # RAPIDJSON_LIBRARIES 6 | 7 | set(RAPIDJSON_SEARCH_PATH "${RAPIDJSON_SOURCE_DIR}" "${CMAKE_SOURCE_DIR}/lib/rapidjson") 8 | 9 | find_path(RAPIDJSON_INCLUDE_DIR 10 | NAMES rapidjson/rapidjson.h 11 | PATHS ${RAPIDJSON_SEARCH_PATH} 12 | PATH_SUFFIXES include) 13 | 14 | set(RAPIDJSON_INCLUDE_DIRS ${RAPIDJSON_INCLUDE_DIR}) 15 | include(FindPackageHandleStandardArgs) 16 | find_package_handle_standard_args(rapidjson DEFAULT_MSG 17 | RAPIDJSON_INCLUDE_DIR 18 | RAPIDJSON_INCLUDE_DIRS) 19 | -------------------------------------------------------------------------------- /api/cmake/FinduWS.cmake: -------------------------------------------------------------------------------- 1 | # - Find uwebsockets 2 | # Find the native uwebsockets includes and libraries 3 | # 4 | # UWS_INCLUDE_DIR - where to find uws.h, etc. 5 | # UWS_LIBRARIES - List of libraries when using uwebsockets. 6 | # UWS_FOUND - True if uwebsockets found. 7 | 8 | # /usr/lib64/libuWS.so 9 | # /usr/include/uWS/uWS.h 10 | 11 | if(UWS_INCLUDE_DIR) 12 | # Already in cache, be silent 13 | set(UWS_FIND_QUIETLY TRUE) 14 | endif(UWS_INCLUDE_DIR) 15 | 16 | find_path(UWS_INCLUDE_DIR uWS/uWS.h) 17 | 18 | find_library(UWS_LIBRARY NAMES libuWS uWS 19 | PATHS /usr/lib 20 | /usr/lib64 21 | /usr/local/lib 22 | /usr/local/lib64) 23 | 24 | # Handle the QUIETLY and REQUIRED arguments and set UWS_FOUND to TRUE if 25 | # all listed variables are TRUE. 26 | include(FindPackageHandleStandardArgs) 27 | find_package_handle_standard_args(UWS DEFAULT_MSG UWS_LIBRARY UWS_INCLUDE_DIR) 28 | 29 | if(UWS_FOUND) 30 | set(UWS_LIBRARIES ${UWS_LIBRARY}) 31 | else(UWS_FOUND) 32 | set(UWS_LIBRARIES) 33 | endif(UWS_FOUND) 34 | 35 | mark_as_advanced(UWS_INCLUDE_DIR UWS_LIBRARY) 36 | -------------------------------------------------------------------------------- /api/container_test.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | 3 | commandTests: 4 | - name: "Startup test" 5 | command: "/bin/bash" 6 | args: ["-c", "cd ~ && touch .env && mkdir public && \ 7 | printf 'API=/api\nSCALA_API=/scala-api\nAPI_WS=\nAFK_TIMEOUT=7200000\nDB_DB=overrustle\nDB_PATH=./overrustle.sqlite\nDONATE_DO_URL=\nDONATE_LINODE_URL=\nDONATE_PAYPAL_URL=\nGITHUB_URL=https://github.com/MemeLabs/Rustla2\nCHAT_URL=https://chat.strims.gg\nCHAT2_DOMAIN=\nCHAT2_URL=\nTHUMBNAIL_REFRESH_INTERVAL=60000\nJWT_SECRET=\nJWT_NAME=jwt\nJWT_DOMAIN=strims.gg\nJWT_TTL=2592000\nPORT=8076\nLIVECHECK_INTERVAL=60000\nTWITCH_CLIENT_ID=\nTWITCH_CLIENT_SECRET=\nTWITCH_REDIRECT_URI=https://example.com/\nEMOTE_SIMILARITY_MIN_LENGTH=4\nEMOTE_SUBSTRING_MIN_LENGTH=2\nEMOTE_SIMILARITY_PREFIX_CHECK_SIZE=2\nEMOTE_SIMILARITY_MIN_EDIT_DISTANCE=4\nEMOTES=ComfyDog,ApeHands,DumpsterFire\nIP_ADDRESS_HEADER=\"x-client-ip\"\nSTREAM_BROADCAST_INTERVAL=60000\nRUSTLER_BROADCAST_INTERVAL=100\nPUBLIC_PATH=./public\n' > .env && \ 8 | nohup /api/rustla2_api --alsologtostderr & sleep 5"] 9 | expectedError: ["server thread"] 10 | exitCode: 0 11 | 12 | - name: "Connection test" 13 | command: "/bin/bash" 14 | args: ["-c", "cd ~ && touch .env && mkdir public && \ 15 | printf 'API=/api\nSCALA_API=/scala-api\nAPI_WS=\nAFK_TIMEOUT=7200000\nDB_DB=overrustle\nDB_PATH=./overrustle.sqlite\nDONATE_DO_URL=\nDONATE_LINODE_URL=\nDONATE_PAYPAL_URL=\nGITHUB_URL=https://github.com/MemeLabs/Rustla2\nCHAT_URL=https://chat.strims.gg\nCHAT2_DOMAIN=\nCHAT2_URL=\nTHUMBNAIL_REFRESH_INTERVAL=60000\nJWT_SECRET=\nJWT_NAME=jwt\nJWT_DOMAIN=strims.gg\nJWT_TTL=2592000\nPORT=8076\nLIVECHECK_INTERVAL=60000\nTWITCH_CLIENT_ID=\nTWITCH_CLIENT_SECRET=\nTWITCH_REDIRECT_URI=https://example.com/\nEMOTE_SIMILARITY_MIN_LENGTH=4\nEMOTE_SUBSTRING_MIN_LENGTH=2\nEMOTE_SIMILARITY_PREFIX_CHECK_SIZE=2\nEMOTE_SIMILARITY_MIN_EDIT_DISTANCE=4\nEMOTES=ComfyDog,ApeHands,DumpsterFire\nIP_ADDRESS_HEADER=\"x-client-ip\"\nSTREAM_BROADCAST_INTERVAL=60000\nRUSTLER_BROADCAST_INTERVAL=100\nPUBLIC_PATH=./public\n' > .env && \ 16 | /api/rustla2_api & sleep 5 && curl -s 0.0.0.0:8076/api"] 17 | expectedOutput: ['{"stream_list":\[\],"streams":{}}'] 18 | exitCode: 0 19 | -------------------------------------------------------------------------------- /api/deps.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Set the `JOBS` environment variable to configure how many `make` jobs can be 4 | # executed simultaneously. If `JOBS` is not set, then the default is the number 5 | # of cores on the system. 6 | nproc=$(getconf _NPROCESSORS_ONLN) 7 | JOBS=${JOBS:-$nproc} 8 | 9 | BASE_DIR="$(/bin/pwd)/third-party" 10 | 11 | set -e 12 | 13 | echo "installing gtest" 14 | 15 | rm -rf "$BASE_DIR/googletest/build" 16 | mkdir "$BASE_DIR/googletest/build" 17 | cd "$BASE_DIR/googletest/build" 18 | cmake .. 19 | make -j $JOBS 20 | make install 21 | 22 | echo "installing folly" 23 | 24 | rm -rf "$BASE_DIR/folly/_build" 25 | mkdir "$BASE_DIR/folly/_build" 26 | cd "$BASE_DIR/folly/_build" 27 | cmake .. 28 | make -j $JOBS 29 | make install 30 | 31 | echo "installing fizz" 32 | 33 | rm -rf "$BASE_DIR/fizz/fizz/_build" 34 | mkdir "$BASE_DIR/fizz/fizz/_build" 35 | cd "$BASE_DIR/fizz/fizz/_build" 36 | cmake .. 37 | make -j $JOBS 38 | make install 39 | 40 | echo "installing wangle" 41 | 42 | cd "$BASE_DIR/wangle/wangle" 43 | cmake . 44 | make -j $JOBS 45 | make install 46 | 47 | echo "installing jansson" 48 | 49 | rm -rf "$BASE_DIR/jansson/build" 50 | mkdir "$BASE_DIR/jansson/build" 51 | cd "$BASE_DIR/jansson/build" 52 | cmake .. 53 | make -j $JOBS 54 | make install 55 | 56 | echo "installing jwt-cpp" 57 | 58 | rm -rf "$BASE_DIR/jwt-cpp/build" 59 | mkdir "$BASE_DIR/jwt-cpp/build" 60 | cd "$BASE_DIR/jwt-cpp/build" 61 | cmake .. 62 | make -j $JOBS 63 | make install 64 | 65 | echo "installing rapidjson" 66 | 67 | rm -rf "$BASE_DIR/rapidjson/build" 68 | mkdir "$BASE_DIR/rapidjson/build" 69 | cd "$BASE_DIR/rapidjson/build" 70 | cmake .. 71 | make -j $JOBS 72 | make install 73 | 74 | echo "installing sqlite_modern_cpp" 75 | 76 | cd "$BASE_DIR/sqlite_modern_cpp" 77 | ./configure 78 | make -j $JOBS 79 | make install 80 | 81 | echo "installing uWebSocket" 82 | 83 | cd "$BASE_DIR/uWebSockets" 84 | make 85 | make install 86 | -------------------------------------------------------------------------------- /api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | rustla2: 5 | build: ./ 6 | image: rustla2:stable 7 | volumes: 8 | - ..:/Rustla2:rw 9 | working_dir: /Rustla2 10 | environment: 11 | GLOG_logtostderr: 1 12 | ports: 13 | - "3000" 14 | network_mode: host 15 | -------------------------------------------------------------------------------- /api/src/APIClient.cpp: -------------------------------------------------------------------------------- 1 | #include "APIClient.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "Status.h" 11 | 12 | namespace rustla2 { 13 | 14 | namespace { 15 | 16 | template 17 | std::string GetDemangledClassName(T* ptr) { 18 | int status; 19 | char* name = abi::__cxa_demangle(typeid(*ptr).name(), 0, 0, &status); 20 | return std::string(name); 21 | } 22 | 23 | } // namespace 24 | 25 | Status APIResult::Validate(const rapidjson::Document& data) { 26 | auto schema = GetSchema(); 27 | if (schema.HasParseError()) { 28 | return Status(StatusCode::JSON_SCHEMA_ERROR, 29 | "invalid json schema in " + GetDemangledClassName(this), 30 | rapidjson::GetParseError_En(schema.GetParseError())); 31 | } 32 | 33 | auto schema_doc = rapidjson::SchemaDocument(schema); 34 | rapidjson::SchemaValidator validator(schema_doc); 35 | 36 | if (data_.HasParseError()) { 37 | return Status(StatusCode::JSON_PARSE_ERROR, 38 | "invalid json response in " + GetDemangledClassName(this), 39 | rapidjson::GetParseError_En(data_.GetParseError())); 40 | } 41 | if (!data_.Accept(validator)) { 42 | rapidjson::StringBuffer doc_uri; 43 | rapidjson::StringBuffer schema_uri; 44 | validator.GetInvalidDocumentPointer().StringifyUriFragment(doc_uri); 45 | validator.GetInvalidSchemaPointer().StringifyUriFragment(schema_uri); 46 | 47 | std::stringstream error_details; 48 | error_details << "invalid " << validator.GetInvalidSchemaKeyword() << ", " 49 | << "document at " << doc_uri.GetString() << " " 50 | << "does not match schema at " << schema_uri.GetString(); 51 | 52 | return Status(StatusCode::VALIDATION_ERROR, "json validation failed", 53 | error_details.str()); 54 | } 55 | 56 | return Status::OK; 57 | } 58 | 59 | Status APIResult::SetData(const char* data, size_t length) { 60 | data_.Parse(data, length); 61 | return Validate(data_); 62 | } 63 | 64 | } // namespace rustla2 65 | -------------------------------------------------------------------------------- /api/src/APIClient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "Curl.h" 7 | #include "Status.h" 8 | 9 | namespace rustla2 { 10 | 11 | class APIResult { 12 | public: 13 | virtual ~APIResult() = default; 14 | 15 | const rapidjson::Document& GetData() const { return data_; } 16 | 17 | virtual rapidjson::Document GetSchema() = 0; 18 | 19 | virtual Status Validate(const rapidjson::Document& data); 20 | 21 | Status SetData(const char* data, size_t length); 22 | 23 | private: 24 | rapidjson::Document data_; 25 | }; 26 | 27 | } // namespace rustla2 28 | -------------------------------------------------------------------------------- /api/src/APIHTTPService.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "DB.h" 7 | #include "HTTPRequest.h" 8 | #include "HTTPRouter.h" 9 | 10 | namespace rustla2 { 11 | 12 | class APIHTTPService { 13 | public: 14 | explicit APIHTTPService(std::shared_ptr db); 15 | 16 | void RegisterRoutes(HTTPRouter *router); 17 | 18 | void GetAPI(uWS::HttpResponse *res, HTTPRequest *req); 19 | 20 | void GetStreamer(uWS::HttpResponse *res, HTTPRequest *req); 21 | 22 | void GetUsername(uWS::HttpResponse *res, HTTPRequest *req); 23 | 24 | void GetProfile(uWS::HttpResponse *res, HTTPRequest *req); 25 | 26 | void PostProfile(uWS::HttpResponse *res, HTTPRequest *req); 27 | 28 | private: 29 | std::shared_ptr db_; 30 | rapidjson::Document profile_update_schema_; 31 | }; 32 | 33 | } // namespace rustla2 34 | -------------------------------------------------------------------------------- /api/src/AdminHTTPService.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "DB.h" 8 | #include "HTTPRequest.h" 9 | #include "HTTPRouter.h" 10 | 11 | namespace rustla2 { 12 | 13 | class AdminHTTPService { 14 | public: 15 | explicit AdminHTTPService(std::shared_ptr db, uWS::Hub *hub); 16 | 17 | void RegisterRoutes(HTTPRouter *router); 18 | 19 | bool RejectNonAdmin(uWS::HttpResponse *res, HTTPRequest *req); 20 | 21 | void PostUsername(uWS::HttpResponse *res, HTTPRequest *req); 22 | 23 | void PostStream(uWS::HttpResponse *res, HTTPRequest *req); 24 | 25 | void GetViewerStates(uWS::HttpResponse *res, HTTPRequest *req); 26 | 27 | void BanViewers(uWS::HttpResponse *res, HTTPRequest *req); 28 | 29 | private: 30 | std::shared_ptr db_; 31 | uWS::Hub *hub_; 32 | rapidjson::Document username_update_schema_; 33 | rapidjson::Document stream_update_schema_; 34 | rapidjson::Document ban_viewer_ips_schema_; 35 | }; 36 | 37 | } // namespace rustla2 38 | -------------------------------------------------------------------------------- /api/src/AngelThumpClient.cpp: -------------------------------------------------------------------------------- 1 | #include "AngelThumpClient.h" 2 | 3 | #include 4 | 5 | #include "Curl.h" 6 | #include "JSON.h" 7 | 8 | namespace rustla2 { 9 | namespace angelthump { 10 | 11 | rapidjson::Document ChannelResult::GetSchema() { 12 | rapidjson::Document schema; 13 | schema.Parse(R"json( 14 | { 15 | "type": "array", 16 | "minItems": 1, 17 | "items": { 18 | "type": "object", 19 | "properties": { 20 | "type": {"type": "string"}, 21 | "thumbnail_url": { 22 | "type": "string", 23 | "format": "uri" 24 | }, 25 | "viewer_count": {"type": "string"}, 26 | "user": { 27 | "type": "object", 28 | "properties": { 29 | "title": {"type": "string"}, 30 | "offline_banner_url": { 31 | "type": "string", 32 | "format": "uri" 33 | }, 34 | "nsfw": {"type": "boolean"} 35 | }, 36 | "required": ["offline_banner_url", "nsfw"] 37 | } 38 | }, 39 | "required": ["user"] 40 | } 41 | } 42 | )json"); 43 | return schema; 44 | } 45 | 46 | const rapidjson::Value& ChannelResult::GetVideoData() const { 47 | return GetData().GetArray()[0]; 48 | } 49 | 50 | std::string ChannelResult::GetTitle() const { 51 | if (GetVideoData()["user"].HasMember("title")) { 52 | return json::StringRef(GetVideoData()["user"]["title"]); 53 | } 54 | return ""; 55 | } 56 | 57 | bool ChannelResult::GetLive() const { 58 | return GetVideoData().HasMember("type") && 59 | json::StringRef(GetVideoData()["type"]) == "live"; 60 | } 61 | 62 | std::string ChannelResult::GetThumbnail() const { 63 | return GetVideoData().HasMember("thumbnail_url") 64 | ? json::StringRef(GetVideoData()["thumbnail_url"]) 65 | : json::StringRef(GetVideoData()["user"]["offline_banner_url"]); 66 | } 67 | 68 | bool ChannelResult::IsNSFW() const { 69 | return GetVideoData()["user"]["nsfw"].GetBool(); 70 | } 71 | 72 | uint64_t ChannelResult::GetViewers() const { 73 | return GetVideoData().HasMember("viewer_count") 74 | ? std::stoull(json::StringRef(GetVideoData()["viewer_count"])) 75 | : 0; 76 | } 77 | 78 | Status Client::GetChannelByName(const std::string& name, 79 | ChannelResult* result) { 80 | CurlRequest req("https://api.angelthump.com/v3/streams?username=" + name); 81 | req.Submit(); 82 | 83 | if (!req.Ok()) { 84 | return Status(StatusCode::HTTP_ERROR, req.GetErrorMessage()); 85 | } 86 | if (req.GetResponseCode() != 200) { 87 | return Status( 88 | StatusCode::API_ERROR, "received non 200 response", 89 | "api returned status code " + std::to_string(req.GetResponseCode())); 90 | } 91 | 92 | const auto& response = req.GetResponse(); 93 | return result->SetData(response.c_str(), response.size()); 94 | } 95 | 96 | } // namespace angelthump 97 | } // namespace rustla2 98 | -------------------------------------------------------------------------------- /api/src/AngelThumpClient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "APIClient.h" 8 | 9 | namespace rustla2 { 10 | namespace angelthump { 11 | 12 | class ChannelResult : public APIResult { 13 | public: 14 | rapidjson::Document GetSchema() final; 15 | 16 | const rapidjson::Value& GetVideoData() const; 17 | 18 | std::string GetTitle() const; 19 | 20 | bool GetLive() const; 21 | 22 | std::string GetThumbnail() const; 23 | 24 | uint64_t GetViewers() const; 25 | 26 | bool IsNSFW() const; 27 | }; 28 | 29 | class Client { 30 | public: 31 | Status GetChannelByName(const std::string& name, ChannelResult* result); 32 | }; 33 | 34 | } // namespace angelthump 35 | } // namespace rustla2 36 | -------------------------------------------------------------------------------- /api/src/AuthHTTPService.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "DB.h" 6 | #include "HTTPRouter.h" 7 | #include "TwitchClient.h" 8 | 9 | namespace rustla2 { 10 | 11 | class AuthHTTPService { 12 | public: 13 | explicit AuthHTTPService(std::shared_ptr db); 14 | 15 | void RegisterRoutes(HTTPRouter *router); 16 | 17 | void GetLogin(uWS::HttpResponse *res, HTTPRequest *req); 18 | 19 | void GetOAuth(uWS::HttpResponse *res, HTTPRequest *req); 20 | 21 | private: 22 | std::shared_ptr db_; 23 | std::unique_ptr twitch_client_; 24 | }; 25 | 26 | } // namespace rustla2 27 | -------------------------------------------------------------------------------- /api/src/BannedStreams.cpp: -------------------------------------------------------------------------------- 1 | #include "BannedStreams.h" 2 | 3 | #include 4 | 5 | namespace rustla2 { 6 | 7 | BannedStreams::BannedStreams(sqlite::database db) : db_(db) { 8 | InitTable(); 9 | 10 | db_ << "SELECT `channel`, `service` FROM `banned_streams`" >> 11 | [&](const std::string& channel, const std::string& service) { 12 | data_.insert(Channel::Create(channel, service)); 13 | }; 14 | 15 | LOG(INFO) << "read " << data_.size() << " banned streams"; 16 | } 17 | 18 | void BannedStreams::InitTable() { 19 | auto query = R"sql( 20 | CREATE TABLE IF NOT EXISTS `banned_streams` ( 21 | `channel` VARCHAR(255) NOT NULL NOT NULL, 22 | `service` VARCHAR(255) NOT NULL NOT NULL, 23 | `reason` VARCHAR(255), 24 | `created_at` DATETIME NOT NULL, 25 | `updated_at` DATETIME NOT NULL, 26 | UNIQUE (`channel`, `service`), 27 | PRIMARY KEY (`channel`, `service`) 28 | ) 29 | )sql"; 30 | db_ << query; 31 | } 32 | 33 | bool BannedStreams::Emplace(const Channel& channel, const std::string& reason) { 34 | try { 35 | auto sql = R"sql( 36 | INSERT INTO `banned_streams` 37 | VALUES (?, ?, ?, datetime(), datetime()) 38 | )sql"; 39 | db_ << sql << channel.GetChannel() << channel.GetService() << reason; 40 | } catch (const sqlite::errors::error& e) { 41 | LOG(ERROR) << "error storing banned stream " 42 | << "channel: " << channel.GetChannel() << ", " 43 | << "service: " << channel.GetService() << ", " 44 | << "reason: " << reason << ", " 45 | << "error: " << e.what(); 46 | 47 | return false; 48 | } 49 | 50 | boost::unique_lock write_lock(lock_); 51 | data_.insert(channel); 52 | 53 | return true; 54 | } 55 | 56 | } // namespace rustla2 57 | -------------------------------------------------------------------------------- /api/src/BannedStreams.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "Channel.h" 10 | 11 | namespace rustla2 { 12 | 13 | class BannedStreams { 14 | public: 15 | explicit BannedStreams(sqlite::database db); 16 | 17 | void InitTable(); 18 | 19 | inline bool Contains(const Channel& channel) { 20 | boost::shared_lock read_lock(lock_); 21 | return data_.count(channel); 22 | } 23 | 24 | bool Emplace(const Channel& channel, const std::string& reason = ""); 25 | 26 | private: 27 | sqlite::database db_; 28 | boost::shared_mutex lock_; 29 | std::unordered_set data_; 30 | }; 31 | 32 | } // namespace rustla2 33 | -------------------------------------------------------------------------------- /api/src/Curl.cpp: -------------------------------------------------------------------------------- 1 | #include "Curl.h" 2 | 3 | #include 4 | 5 | namespace rustla2 { 6 | 7 | CurlRequest::CurlRequest(const std::string &url) : headers_(nullptr) { 8 | curl_ = curl_easy_init(); 9 | if (curl_ == nullptr) { 10 | error_code_ = CURLE_FAILED_INIT; 11 | return; 12 | } 13 | error_code_ = CURLE_OK; 14 | 15 | curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); 16 | curl_easy_setopt(curl_, CURLOPT_NOSIGNAL, 1); 17 | curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, 3); 18 | curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 3); 19 | curl_easy_setopt(curl_, CURLOPT_ACCEPT_ENCODING, ""); 20 | curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, CurlRequest::WriteCallback); 21 | curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response_data_); 22 | } 23 | 24 | CurlRequest::~CurlRequest() { 25 | if (curl_) { 26 | curl_easy_cleanup(curl_); 27 | } 28 | } 29 | 30 | void CurlRequest::EnableDebug() { 31 | curl_easy_setopt(curl_, CURLOPT_VERBOSE, true); 32 | } 33 | 34 | void CurlRequest::AddHeader(const std::string &data) { 35 | headers_ = curl_slist_append(headers_, data.c_str()); 36 | } 37 | 38 | void CurlRequest::SetPostData(std::string data) { 39 | SetPostData(data.c_str(), data.size()); 40 | } 41 | 42 | void CurlRequest::SetPostData(const char *data, size_t size) { 43 | curl_easy_setopt(curl_, CURLOPT_POST, 1); 44 | curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, size); 45 | curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data); 46 | } 47 | 48 | bool CurlRequest::Submit() { 49 | curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers_); 50 | error_code_ = curl_easy_perform(curl_); 51 | curl_slist_free_all(headers_); 52 | 53 | return Ok(); 54 | } 55 | 56 | bool CurlRequest::Ok() const { return error_code_ == CURLE_OK; } 57 | 58 | bool CurlRequest::GetErrorCode() const { return error_code_; } 59 | 60 | std::string CurlRequest::GetErrorMessage() const { 61 | const char *message = curl_easy_strerror(error_code_); 62 | return std::string(message, strlen(message)); 63 | } 64 | 65 | std::string CurlRequest::GetResponse() const { return response_data_.str(); } 66 | 67 | int64_t CurlRequest::GetResponseCode() const { 68 | int64_t code; 69 | curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &code); 70 | return code; 71 | } 72 | 73 | size_t CurlRequest::WriteCallback(char *src, size_t size, size_t nmemb, 74 | void *dst) { 75 | auto *data = (std::stringstream *)dst; 76 | data->write(src, size * nmemb); 77 | return size * nmemb; 78 | } 79 | 80 | } // namespace rustla2 81 | -------------------------------------------------------------------------------- /api/src/Curl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace rustla2 { 9 | 10 | class CurlRequest { 11 | public: 12 | explicit CurlRequest(const std::string &url); 13 | 14 | ~CurlRequest(); 15 | 16 | void EnableDebug(); 17 | 18 | void AddHeader(const std::string &data); 19 | 20 | void SetPostData(std::string data); 21 | 22 | void SetPostData(const char *data, size_t size); 23 | 24 | bool Submit(); 25 | 26 | bool Ok() const; 27 | 28 | bool GetErrorCode() const; 29 | 30 | std::string GetErrorMessage() const; 31 | 32 | std::string GetResponse() const; 33 | 34 | int64_t GetResponseCode() const; 35 | 36 | static size_t WriteCallback(char *src, size_t size, size_t nmemb, void *dst); 37 | 38 | private: 39 | CURL *curl_; 40 | curl_slist *headers_; 41 | std::stringstream response_data_; 42 | CURLcode error_code_; 43 | }; 44 | 45 | } // namespace rustla2 46 | -------------------------------------------------------------------------------- /api/src/DB.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "BannedStreams.h" 7 | #include "Config.h" 8 | #include "IPRanges.h" 9 | #include "Streams.h" 10 | #include "Users.h" 11 | #include "ViewerStates.h" 12 | 13 | namespace rustla2 { 14 | 15 | class DB { 16 | public: 17 | DB() 18 | : db_(Config::Get().GetDBPath()), 19 | users_(std::make_shared(db_)), 20 | banned_streams_(std::make_shared(db_)), 21 | banned_ips_(std::make_shared(db_, "banned_ip_ranges")), 22 | streams_(std::make_shared(db_)), 23 | viewer_states_(std::make_shared(users_, streams_)) {} 24 | 25 | inline std::shared_ptr GetUsers() { return users_; } 26 | 27 | inline std::shared_ptr GetBannedStreams() { 28 | return banned_streams_; 29 | } 30 | 31 | inline std::shared_ptr GetBannedIPs() { return banned_ips_; } 32 | 33 | inline std::shared_ptr GetStreams() { return streams_; } 34 | 35 | inline std::shared_ptr GetViewerStates() { 36 | return viewer_states_; 37 | } 38 | 39 | private: 40 | sqlite::database db_; 41 | std::shared_ptr users_; 42 | std::shared_ptr banned_streams_; 43 | std::shared_ptr banned_ips_; 44 | std::shared_ptr streams_; 45 | std::shared_ptr viewer_states_; 46 | }; 47 | 48 | } // namespace rustla2 49 | -------------------------------------------------------------------------------- /api/src/HTTPRequest.cpp: -------------------------------------------------------------------------------- 1 | #include "HTTPRequest.h" 2 | 3 | #include 4 | 5 | #include "Config.h" 6 | #include "Session.h" 7 | 8 | namespace rustla2 { 9 | 10 | namespace { 11 | const boost::regex query_regex("(?:^|&)([^=]+)=([^&]+)"); 12 | } 13 | 14 | HTTPRequest::HTTPRequest(uWS::HttpRequest req) : req_(req) { 15 | const auto& url = req.getUrl(); 16 | folly::StringPiece uri(url.value, url.valueLength); 17 | folly::StringPiece path, query; 18 | if (folly::split('?', uri, path, query)) { 19 | folly::split('/', path, path_); 20 | query_ = query; 21 | } else { 22 | folly::split('/', uri, path_); 23 | } 24 | } 25 | 26 | HTTPRequest::HTTPRequest(rustla2::HTTPRequest&& req) noexcept 27 | : req_(std::move(req.req_)), 28 | path_(std::move(req.path_)), 29 | query_(std::move(req.query_)), 30 | post_data_(std::move(req.post_data_)), 31 | keep_alive_(std::move(req.keep_alive_)), 32 | post_data_handler_(std::move(req.post_data_handler_)), 33 | cancel_handler_(std::move(req.cancel_handler_)) {} 34 | 35 | void HTTPRequest::WritePostData(char* data, size_t length, 36 | size_t remaining_bytes) { 37 | post_data_.append(data, length); 38 | 39 | if (remaining_bytes == 0 && post_data_handler_) { 40 | post_data_handler_(post_data_.data(), post_data_.size()); 41 | } 42 | } 43 | 44 | void HTTPRequest::EmitCancel() { 45 | if (cancel_handler_) { 46 | cancel_handler_(); 47 | } 48 | } 49 | 50 | const std::map HTTPRequest::GetQueryParams() const { 51 | std::map params; 52 | boost::cregex_iterator front(query_.begin(), query_.end(), query_regex); 53 | boost::cregex_iterator back; 54 | for (auto i = front; i != back; ++i) { 55 | params.insert({std::string((*i)[1].first, (*i)[1].second), 56 | std::string((*i)[2].first, (*i)[2].second)}); 57 | } 58 | return params; 59 | } 60 | 61 | std::string HTTPRequest::GetCookie(const std::string& name) { 62 | auto header = req_.getHeader("cookie"); 63 | const folly::StringPiece cookie(header.value, header.valueLength); 64 | 65 | const std::string prefix = name + "="; 66 | size_t offset = cookie.find(prefix); 67 | if (offset == std::string::npos) { 68 | return ""; 69 | } 70 | size_t start = offset + prefix.size(); 71 | 72 | size_t end = cookie.find(";", start); 73 | if (end == std::string::npos) { 74 | end = cookie.size(); 75 | } 76 | 77 | return cookie.subpiece(start, end - start).toString(); 78 | } 79 | 80 | std::string HTTPRequest::GetSessionID() { 81 | auto cookie = GetCookie(Config::Get().GetJWTName()); 82 | return DecodeSessionCookie(cookie); 83 | } 84 | 85 | folly::StringPiece HTTPRequest::GetClientIPHeader() { 86 | auto header = req_.getHeader(Config::Get().GetIPAddressHeader().c_str()); 87 | return folly::StringPiece(header.value, header.valueLength); 88 | } 89 | 90 | } // namespace rustla2 91 | -------------------------------------------------------------------------------- /api/src/HTTPRequest.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "Config.h" 10 | #include "Session.h" 11 | 12 | namespace rustla2 { 13 | 14 | using PostDataHandler = std::function; 15 | using CancelHandler = std::function; 16 | 17 | class HTTPRequest { 18 | public: 19 | explicit HTTPRequest(uWS::HttpRequest req); 20 | 21 | explicit HTTPRequest(rustla2::HTTPRequest&& req) noexcept; 22 | 23 | inline void OnPostData(PostDataHandler handler) { 24 | post_data_handler_ = handler; 25 | } 26 | 27 | void WritePostData(char* data, size_t length, size_t remaining_bytes); 28 | 29 | inline void OnCancel(CancelHandler handler) { cancel_handler_ = handler; } 30 | 31 | void EmitCancel(); 32 | 33 | void SetKeepAlive(bool keep_alive) { keep_alive_ = keep_alive; } 34 | 35 | bool GetKeepAlive() { return keep_alive_; } 36 | 37 | const std::map GetQueryParams() const; 38 | 39 | inline const uWS::HttpMethod GetMethod() { return req_.getMethod(); } 40 | 41 | std::string GetCookie(const std::string& name); 42 | 43 | std::string GetSessionID(); 44 | 45 | folly::StringPiece GetClientIPHeader(); 46 | 47 | inline const std::vector& GetPath() const { 48 | return path_; 49 | } 50 | 51 | inline folly::StringPiece GetPathPart(size_t i) const { return path_[i]; } 52 | 53 | private: 54 | uWS::HttpRequest req_; 55 | std::vector path_; 56 | folly::StringPiece query_; 57 | std::string post_data_; 58 | bool keep_alive_; 59 | PostDataHandler post_data_handler_; 60 | CancelHandler cancel_handler_; 61 | }; 62 | 63 | } // namespace rustla2 64 | -------------------------------------------------------------------------------- /api/src/HTTPResponseWriter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace rustla2 { 11 | 12 | class WSHTTPResponseProxy : public std::stringbuf { 13 | public: 14 | explicit WSHTTPResponseProxy(uWS::HttpResponse* res) : res_(res) {} 15 | 16 | ~WSHTTPResponseProxy() { 17 | if (res_) res_->end(); 18 | } 19 | 20 | protected: 21 | virtual int sync() override final { 22 | auto buf = str(); 23 | res_->write(buf.data(), buf.size()); 24 | str(""); 25 | return 0; 26 | } 27 | 28 | private: 29 | uWS::HttpResponse* res_; 30 | }; 31 | 32 | class HTTPResponseWriter { 33 | public: 34 | explicit HTTPResponseWriter(std::stringstream& res) 35 | : proxy_(nullptr), res_(res.rdbuf()) {} 36 | 37 | explicit HTTPResponseWriter(uWS::HttpResponse* res) 38 | : proxy_(res), res_(&proxy_) {} 39 | 40 | void Status(const uint32_t code, const std::string& label); 41 | 42 | void Header(const std::string& name, const std::tm* value); 43 | 44 | void Header(const std::string& name, const std::string& value); 45 | 46 | void Header(const std::string& name, const int64_t value); 47 | 48 | void Cookie(const std::string& name, const std::string& value, 49 | const std::string& domain = "", const time_t max_age = 0, 50 | const bool http_only = false, const bool secure = false); 51 | 52 | void SessionCookie(const std::string& id); 53 | 54 | void Body(const std::string& body = "") { Body(body.data(), body.size()); } 55 | 56 | void Body(const char* body, const size_t size); 57 | 58 | void JSON(const std::string& data); 59 | 60 | void LocalFile(const boost::filesystem::path& path); 61 | 62 | private: 63 | WSHTTPResponseProxy proxy_; 64 | std::ostream res_; 65 | }; 66 | 67 | } // namespace rustla2 68 | -------------------------------------------------------------------------------- /api/src/HTTPService.cpp: -------------------------------------------------------------------------------- 1 | #include "HTTPService.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "Config.h" 8 | #include "HTTPResponseWriter.h" 9 | 10 | namespace rustla2 { 11 | 12 | HTTPService::HTTPService(std::shared_ptr db, uWS::Hub *hub) 13 | : db_(db), 14 | admin_service_(db_, hub), 15 | api_service_(db_), 16 | auth_service_(db_), 17 | static_service_(Config::Get().GetPublicPath()) { 18 | admin_service_.RegisterRoutes(&router_); 19 | api_service_.RegisterRoutes(&router_); 20 | auth_service_.RegisterRoutes(&router_); 21 | static_service_.RegisterRoutes(&router_); 22 | 23 | hub->onHttpRequest([&](uWS::HttpResponse *res, uWS::HttpRequest uws_req, 24 | char *data, size_t length, size_t remaining_bytes) { 25 | HTTPRequest req(uws_req); 26 | 27 | if (RejectBannedIP(res, &req)) { 28 | return; 29 | } 30 | 31 | if (!router_.Dispatch(res, &req)) { 32 | static_service_.ServeIndex(res); 33 | return; 34 | } 35 | req.WritePostData(data, length, remaining_bytes); 36 | 37 | if (remaining_bytes != 0 || req.GetKeepAlive()) { 38 | res->setUserData(new HTTPRequest(std::move(req))); 39 | } 40 | }); 41 | 42 | hub->onHttpData([&](uWS::HttpResponse *res, char *data, size_t length, 43 | size_t remaining_bytes) { 44 | auto req = LoadHTTPRequestFromUserData(res); 45 | if (req != nullptr) { 46 | req->WritePostData(data, length, remaining_bytes); 47 | 48 | if (remaining_bytes == 0) { 49 | delete req; 50 | res->setUserData(nullptr); 51 | } 52 | } 53 | }); 54 | 55 | hub->onCancelledHttpRequest([&](uWS::HttpResponse *res) { 56 | auto req = LoadHTTPRequestFromUserData(res); 57 | if (req != nullptr) { 58 | req->EmitCancel(); 59 | delete req; 60 | res->setUserData(nullptr); 61 | } 62 | }); 63 | } 64 | 65 | bool HTTPService::RejectBannedIP(uWS::HttpResponse *res, HTTPRequest *req) { 66 | if (db_->GetBannedIPs()->Contains(req->GetClientIPHeader())) { 67 | HTTPResponseWriter writer(res); 68 | writer.Status(403, "Forbidden"); 69 | writer.Body(); 70 | return true; 71 | } 72 | 73 | return false; 74 | } 75 | 76 | } // namespace rustla2 77 | -------------------------------------------------------------------------------- /api/src/HTTPService.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "APIHTTPService.h" 5 | #include "AdminHTTPService.h" 6 | #include "AuthHTTPService.h" 7 | #include "DB.h" 8 | #include "HTTPRequest.h" 9 | #include "HTTPRouter.h" 10 | #include "StaticHTTPService.h" 11 | 12 | namespace rustla2 { 13 | 14 | class HTTPService { 15 | public: 16 | HTTPService(std::shared_ptr db, uWS::Hub *hub); 17 | 18 | private: 19 | inline HTTPRequest *LoadHTTPRequestFromUserData(uWS::HttpResponse *res) { 20 | return res == nullptr ? nullptr 21 | : static_cast(res->getUserData()); 22 | } 23 | 24 | bool RejectBannedIP(uWS::HttpResponse *res, HTTPRequest *req); 25 | 26 | std::shared_ptr db_; 27 | HTTPRouter router_; 28 | AdminHTTPService admin_service_; 29 | APIHTTPService api_service_; 30 | AuthHTTPService auth_service_; 31 | StaticHTTPService static_service_; 32 | }; 33 | 34 | } // namespace rustla2 35 | -------------------------------------------------------------------------------- /api/src/IPRanges.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace rustla2 { 15 | 16 | class IPRanges; 17 | 18 | class IPSet { 19 | public: 20 | uint32_t Incr(const std::string& address_str); 21 | uint32_t Decr(const std::string& address_str); 22 | 23 | private: 24 | std::unordered_map counts_; 25 | mutable boost::shared_mutex lock_; 26 | 27 | friend class IPRanges; 28 | }; 29 | 30 | class IPRanges { 31 | public: 32 | using ValueRanges = boost::icl::interval_set; 33 | using Value = ValueRanges::interval_type; 34 | 35 | IPRanges(sqlite::database db, const std::string& table_name); 36 | 37 | void InitTable(); 38 | 39 | bool Contains(const folly::StringPiece address_str); 40 | 41 | bool Insert(const std::string& address_str) { 42 | return Insert(address_str, address_str); 43 | } 44 | 45 | bool Insert(std::shared_ptr ip_set, const std::string& note = "") { 46 | boost::shared_lock read_lock(ip_set->lock_); 47 | for (auto it : ip_set->counts_) { 48 | Insert(it.first, it.first, note); 49 | } 50 | return true; 51 | } 52 | 53 | bool Insert(const std::string& range_start_str, 54 | const std::string& range_end_str, const std::string& note = ""); 55 | 56 | private: 57 | sqlite::database db_; 58 | const std::string table_name_; 59 | boost::shared_mutex lock_; 60 | ValueRanges data_; 61 | }; 62 | 63 | namespace internal { 64 | 65 | unsigned __int128 GetAddressValue(const folly::StringPiece address_str); 66 | 67 | template 68 | inline unsigned __int128 PackAddressBytes(T bytes, size_t length) { 69 | // prefix ipv4 addresses with 0xffff 70 | unsigned __int128 value = std::numeric_limits::max(); 71 | 72 | for (int i = 0; i < length; ++i) { 73 | value = (value << 8) | bytes[i]; 74 | } 75 | return value; 76 | } 77 | 78 | } // namespace internal 79 | } // namespace rustla2 80 | -------------------------------------------------------------------------------- /api/src/JSON.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace rustla2 { 10 | namespace json { 11 | 12 | template 13 | std::string Serialize(const T* model) { 14 | rapidjson::StringBuffer buf; 15 | rapidjson::Writer writer(buf); 16 | model->WriteJSON(&writer); 17 | return buf.GetString(); 18 | } 19 | 20 | template 21 | std::string Serialize(const T& model) { 22 | return Serialize(&model); 23 | } 24 | 25 | template 26 | std::string Serialize(std::shared_ptr model) { 27 | return Serialize(const_cast(model.get())); 28 | } 29 | 30 | struct StringRef { 31 | template 32 | explicit StringRef(const T& value) 33 | : string_(value.GetString()), size_(value.GetStringLength()) {} 34 | 35 | inline operator std::string() const { return std::string(string_, size_); } 36 | 37 | bool operator==(const std::string& rhs) const { 38 | return rhs.size() == size_ && rhs == string_; 39 | } 40 | 41 | bool operator!=(const std::string& rhs) const { 42 | return rhs.size() != size_ || rhs != string_; 43 | } 44 | 45 | bool Empty() const { return size_ == 0; } 46 | 47 | const char* string_; 48 | const size_t size_; 49 | }; 50 | 51 | } // namespace json 52 | } // namespace rustla2 53 | -------------------------------------------------------------------------------- /api/src/MIMETypes.cpp: -------------------------------------------------------------------------------- 1 | #include "MIMETypes.h" 2 | 3 | #include 4 | 5 | namespace rustla2 { 6 | 7 | MIMETypes::MIMETypes() { 8 | magic_ = magic_open(MAGIC_MIME_TYPE); 9 | magic_load(magic_, nullptr); 10 | magic_compile(magic_, nullptr); 11 | 12 | for (const auto& t : mime_types) { 13 | mime_map_[t.extension.toString()] = t.name; 14 | } 15 | } 16 | 17 | MIMETypes::~MIMETypes() { 18 | magic_close(magic_); 19 | magic_ = nullptr; 20 | } 21 | 22 | std::string MIMETypes::Get(const std::string& path) { 23 | auto i = mime_map_.find(boost::filesystem::extension(path)); 24 | auto mime = 25 | i != mime_map_.end() ? i->second : magic_file(magic_, path.c_str()); 26 | 27 | return mime.toString(); 28 | } 29 | 30 | } // namespace rustla2 31 | -------------------------------------------------------------------------------- /api/src/MIMETypes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace rustla2 { 9 | 10 | class MIMETypes { 11 | public: 12 | MIMETypes(); 13 | ~MIMETypes(); 14 | 15 | std::string Get(const std::string& path); 16 | 17 | private: 18 | magic_t magic_; 19 | std::map mime_map_; 20 | }; 21 | 22 | struct MIMEType { 23 | folly::StringPiece extension; 24 | folly::StringPiece name; 25 | }; 26 | 27 | constexpr MIMEType mime_types[] = { 28 | {".aac", "audio/aac"}, 29 | {".abw", "application/x-abiword"}, 30 | {".arc", "application/octet-stream"}, 31 | {".avi", "video/x-msvideo"}, 32 | {".azw", "application/vnd.amazon.ebook"}, 33 | {".bin", "application/octet-stream"}, 34 | {".bz", "application/x-bzip"}, 35 | {".bz2", "application/x-bzip2"}, 36 | {".csh", "application/x-csh"}, 37 | {".css", "text/css"}, 38 | {".csv", "text/csv"}, 39 | {".doc", "application/msword"}, 40 | {".eot", "application/vnd.ms-fontobject"}, 41 | {".epub", "application/epub+zip"}, 42 | {".gif", "image/gif"}, 43 | {".htm", "text/html"}, 44 | {".html", "text/html"}, 45 | {".ico", "image/x-icon"}, 46 | {".ics", "text/calendar"}, 47 | {".jar", "application/java-archive"}, 48 | {".jpeg", "image/jpeg"}, 49 | {".jpg", "image/jpeg"}, 50 | {".js", "application/javascript"}, 51 | {".json", "application/json"}, 52 | {".mid", "audio/midi"}, 53 | {".midi", "audio/midi"}, 54 | {".mpeg", "video/mpeg"}, 55 | {".mpkg", "application/vnd.apple.installer+xml"}, 56 | {".odp", "application/vnd.oasis.opendocument.presentation"}, 57 | {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, 58 | {".odt", "application/vnd.oasis.opendocument.text"}, 59 | {".oga", "audio/ogg"}, 60 | {".ogv", "video/ogg"}, 61 | {".ogx", "application/ogg"}, 62 | {".otf", "font/otf"}, 63 | {".png", "image/png"}, 64 | {".pdf", "application/pdf"}, 65 | {".ppt", "application/vnd.ms-powerpoint"}, 66 | {".rar", "application/x-rar-compressed"}, 67 | {".rtf", "application/rtf"}, 68 | {".sh", "application/x-sh"}, 69 | {".svg", "image/svg+xml"}, 70 | {".swf", "application/x-shockwave-flash"}, 71 | {".tar", "application/x-tar"}, 72 | {".tif", "image/tiff"}, 73 | {".tiff", "image/tiff"}, 74 | {".ts", "application/typescript"}, 75 | {".ttf", "font/ttf"}, 76 | {".vsd", "application/vnd.visio"}, 77 | {".wav", "audio/x-wav"}, 78 | {".weba", "audio/webm"}, 79 | {".webm", "video/webm"}, 80 | {".webp", "image/webp"}, 81 | {".woff", "font/woff"}, 82 | {".woff2", "font/woff2"}, 83 | {".xhtml", "application/xhtml+xml"}, 84 | {".xls", "application/vnd.ms-excel"}, 85 | {".xlsx", 86 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, 87 | {".xml", "application/xml"}, 88 | {".xul", "application/vnd.mozilla.xul+xml"}, 89 | {".zip", "application/zip"}, 90 | {".3gp", "video/3gpp"}, 91 | {".3gp", "audio/3gpp"}, 92 | {".3g2", "video/3gpp2"}, 93 | {".3g2", "audio/3gpp2"}, 94 | {".7z", "application/x-7z-compressed"}}; 95 | 96 | } // namespace rustla2 97 | -------------------------------------------------------------------------------- /api/src/Observer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace rustla2 { 9 | 10 | template 11 | class Observer { 12 | public: 13 | Observer(std::uint64_t id, std::set keys = {}) : id_(id) { 14 | std::copy(keys.begin(), keys.end(), 15 | std::inserter(changed_keys_, changed_keys_.begin())); 16 | }; 17 | 18 | std::uint64_t GetID() { return id_; } 19 | 20 | void Mark(const T &key) { 21 | boost::lock_guard lock{lock_}; 22 | changed_keys_.insert(key); 23 | } 24 | 25 | bool Next(T *key) { 26 | boost::lock_guard lock{lock_}; 27 | 28 | auto next_key = changed_keys_.begin(); 29 | if (next_key == changed_keys_.end()) { 30 | return false; 31 | } 32 | 33 | *key = *next_key; 34 | changed_keys_.erase(*key); 35 | 36 | return true; 37 | } 38 | 39 | private: 40 | boost::mutex lock_; 41 | std::uint64_t id_; 42 | std::set changed_keys_; 43 | }; 44 | 45 | template 46 | class Observable { 47 | public: 48 | std::shared_ptr> CreateObserver(std::set keys = {}) { 49 | boost::lock_guard lock{lock_}; 50 | 51 | auto id = observer_id_++; 52 | auto observer = std::make_shared>(id, keys); 53 | observers_[id] = observer; 54 | 55 | return observer; 56 | } 57 | 58 | void StopObserving(std::shared_ptr> observer) { 59 | boost::lock_guard lock{lock_}; 60 | observers_.erase(observer->GetID()); 61 | } 62 | 63 | void Mark(const T &key) { 64 | boost::lock_guard lock{lock_}; 65 | for (auto i : observers_) { 66 | i.second->Mark(key); 67 | } 68 | } 69 | 70 | private: 71 | boost::mutex lock_; 72 | std::uint64_t observer_id_{0}; 73 | std::map>> observers_; 74 | }; 75 | 76 | } // namespace rustla2 77 | -------------------------------------------------------------------------------- /api/src/ServicePoller.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "APIClient.h" 7 | #include "DB.h" 8 | #include "SmashcastClient.h" 9 | #include "Status.h" 10 | #include "TwitchClient.h" 11 | #include "YoutubeClient.h" 12 | 13 | namespace rustla2 { 14 | 15 | struct ChannelState { 16 | ChannelState() : thumbnail(""), viewers(0), live(false), nsfw(false) {} 17 | 18 | std::string title; 19 | std::string thumbnail; 20 | uint64_t viewers; 21 | bool live; 22 | bool nsfw; 23 | }; 24 | 25 | class ServicePoller { 26 | public: 27 | explicit ServicePoller(std::shared_ptr db); 28 | 29 | void Run(); 30 | 31 | const Status CheckAngelThump(const std::string& name, ChannelState* state); 32 | 33 | const Status CheckM3u8(const std::string& name, ChannelState* state); 34 | 35 | const Status CheckTwitchStream(const std::string& name, ChannelState* state); 36 | 37 | const Status CheckTwitchVOD(const std::string& name, ChannelState* state); 38 | 39 | const Status CheckYouTube(const std::string& name, ChannelState* state); 40 | 41 | const Status CheckSmashcast(const std::string& name, ChannelState* state); 42 | 43 | private: 44 | std::shared_ptr db_; 45 | std::unique_ptr twitch_; 46 | std::unique_ptr youtube_; 47 | }; 48 | 49 | } // namespace rustla2 50 | -------------------------------------------------------------------------------- /api/src/Session.cpp: -------------------------------------------------------------------------------- 1 | #include "Session.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "Config.h" 8 | 9 | namespace rustla2 { 10 | 11 | std::string EncodeSessionCookie(const std::string& id) { 12 | const time_t eol = time(nullptr) + Config::Get().GetJWTTTL(); 13 | nlohmann::json json = {{"id", id}, {"exp", eol}}; 14 | 15 | HS256Validator signer(Config::Get().GetJWTSecret()); 16 | return JWT::Encode(signer, json); 17 | } 18 | 19 | std::string DecodeSessionCookie(const std::string& cookie) { 20 | if (cookie.empty()) { 21 | return ""; 22 | } 23 | 24 | nlohmann::json header, payload; 25 | ExpValidator exp; 26 | HS256Validator signer(Config::Get().GetJWTSecret()); 27 | try { 28 | std::tie(header, payload) = JWT::Decode(cookie, &signer, &exp); 29 | } catch (InvalidTokenError& e) { 30 | return ""; 31 | } 32 | 33 | auto id = payload.find("id"); 34 | if (id == payload.end() || !id->is_string()) { 35 | return ""; 36 | } 37 | 38 | return id->get(); 39 | } 40 | 41 | } // namespace rustla2 42 | -------------------------------------------------------------------------------- /api/src/Session.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace rustla2 { 6 | 7 | std::string EncodeSessionCookie(const std::string& id); 8 | 9 | std::string DecodeSessionCookie(const std::string& cookie); 10 | 11 | } // namespace rustla2 12 | -------------------------------------------------------------------------------- /api/src/SmashcastClient.cpp: -------------------------------------------------------------------------------- 1 | #include "SmashcastClient.h" 2 | 3 | #include "Curl.h" 4 | #include "JSON.h" 5 | 6 | namespace rustla2 { 7 | namespace smashcast { 8 | rapidjson::Document ChannelResult::GetSchema() { 9 | rapidjson::Document schema; 10 | schema.Parse(R"json( 11 | { 12 | "type": "object", 13 | "properties": { 14 | "livestream": { 15 | "type": "array", 16 | "items": { 17 | "type": "object", 18 | "properties": { 19 | "media_status": {"type": "string"}, 20 | "media_is_live": {"type": "string"}, 21 | "media_thumbnail": {"type": "string"}, 22 | "media_views": { 23 | "type": "string", 24 | "pattern": "^[0-9]+$" 25 | } 26 | }, 27 | "required": [ 28 | "media_status", 29 | "media_is_live", 30 | "media_thumbnail", 31 | "media_views" 32 | ] 33 | }, 34 | "minItems": 1 35 | } 36 | }, 37 | "required": ["livestream"] 38 | } 39 | )json"); 40 | return schema; 41 | } 42 | 43 | std::string ChannelResult::GetTitle() const { 44 | return json::StringRef(GetLivestream()["media_status"]); 45 | } 46 | 47 | bool ChannelResult::GetLive() const { 48 | return json::StringRef(GetLivestream()["media_is_live"]) == "1"; 49 | } 50 | 51 | std::string ChannelResult::GetThumbnail() const { 52 | return "https://edge.sf.hitbox.tv" + 53 | std::string(json::StringRef(GetLivestream()["media_thumbnail"])); 54 | } 55 | 56 | uint64_t ChannelResult::GetViewers() const { 57 | return std::stoull( 58 | std::string(json::StringRef(GetLivestream()["media_views"]))); 59 | } 60 | 61 | const rapidjson::Value& ChannelResult::GetLivestream() const { 62 | return GetData()["livestream"][0]; 63 | } 64 | 65 | Status Client::GetChannelByName(const std::string& name, 66 | ChannelResult* result) { 67 | CurlRequest req("https://api.smashcast.tv/media/live/" + name); 68 | req.Submit(); 69 | 70 | if (!req.Ok()) { 71 | return Status(StatusCode::HTTP_ERROR, req.GetErrorMessage()); 72 | } 73 | 74 | if (req.GetResponseCode() != 200) { 75 | return Status( 76 | StatusCode::API_ERROR, "received non 200 response", 77 | "api returned status code " + std::to_string(req.GetResponseCode())); 78 | } 79 | 80 | const auto& response = req.GetResponse(); 81 | return result->SetData(response.c_str(), response.size()); 82 | } 83 | 84 | } // namespace smashcast 85 | } // namespace rustla2 86 | -------------------------------------------------------------------------------- /api/src/SmashcastClient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "APIClient.h" 7 | #include "Curl.h" 8 | #include "Status.h" 9 | 10 | namespace rustla2 { 11 | namespace smashcast { 12 | 13 | class ChannelResult : public APIResult { 14 | public: 15 | rapidjson::Document GetSchema() final; 16 | 17 | std::string GetTitle() const; 18 | 19 | bool GetLive() const; 20 | 21 | std::string GetThumbnail() const; 22 | 23 | uint64_t GetViewers() const; 24 | 25 | const rapidjson::Value& GetLivestream() const; 26 | }; 27 | 28 | class Client { 29 | public: 30 | Status GetChannelByName(const std::string& name, ChannelResult* result); 31 | }; 32 | 33 | } // namespace smashcast 34 | } // namespace rustla2 35 | -------------------------------------------------------------------------------- /api/src/StaticHTTPService.cpp: -------------------------------------------------------------------------------- 1 | #include "StaticHTTPService.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "HTTPRequest.h" 10 | #include "HTTPResponseWriter.h" 11 | 12 | namespace rustla2 { 13 | 14 | const fs::path StaticCacheEntry::VOID_PATH = ""; 15 | 16 | StaticCacheEntry::StaticCacheEntry(const fs::path& path) { 17 | std::stringstream buf; 18 | HTTPResponseWriter writer(buf); 19 | writer.Status(200, "OK"); 20 | writer.Header("Cache-Control", "max-age=3600, public"); 21 | 22 | if (path == StaticCacheEntry::VOID_PATH) { 23 | writer.Body("", 0); 24 | } else { 25 | writer.LocalFile(path); 26 | } 27 | 28 | data_ = buf.str(); 29 | data_size_ = data_.size(); 30 | 31 | const std::string header_delimiter = "\r\n\r\n"; 32 | header_size_ = data_.find(header_delimiter) + header_delimiter.size(); 33 | } 34 | 35 | StaticHTTPService::StaticHTTPService(const std::string& root_dir, 36 | const std::string& index) { 37 | cache_["/"].reset(new StaticCacheEntry(StaticCacheEntry::VOID_PATH)); 38 | 39 | if (!fs::is_directory(root_dir)) { 40 | LOG(ERROR) << "http server path does not exist (" << root_dir << ")"; 41 | return; 42 | } 43 | 44 | for (fs::recursive_directory_iterator 45 | i = fs::recursive_directory_iterator(fs::path(root_dir)), 46 | end_iter; 47 | i != end_iter; i++) { 48 | auto path = i->path(); 49 | if (fs::is_regular_file(path)) { 50 | folly::StringPiece server_path(path.string()); 51 | server_path.removePrefix(root_dir); 52 | server_path.removeSuffix(index); 53 | 54 | cache_[server_path.toString()].reset(new StaticCacheEntry(path)); 55 | } 56 | } 57 | } 58 | 59 | void StaticHTTPService::RegisterRoutes(HTTPRouter* router) { 60 | for (const auto& i : cache_) { 61 | const auto cache = i.second; 62 | router->Get(i.first, [=](uWS::HttpResponse* res, HTTPRequest* req) { 63 | res->write(cache->Data(), cache->Size()); 64 | res->end(); 65 | }); 66 | 67 | router->Head(i.first, [=](uWS::HttpResponse* res, HTTPRequest* req) { 68 | res->write(cache->Data(), cache->HeaderSize()); 69 | res->end(); 70 | }); 71 | } 72 | } 73 | 74 | void StaticHTTPService::ServeIndex(uWS::HttpResponse* res) { 75 | const auto& cache = cache_["/"]; 76 | res->write(cache->Data(), cache->Size()); 77 | res->end(); 78 | } 79 | 80 | } // namespace rustla2 81 | -------------------------------------------------------------------------------- /api/src/StaticHTTPService.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "HTTPRouter.h" 9 | 10 | namespace rustla2 { 11 | 12 | namespace fs = boost::filesystem; 13 | 14 | class StaticCacheEntry { 15 | public: 16 | static const fs::path VOID_PATH; 17 | 18 | explicit StaticCacheEntry(const fs::path& path); 19 | 20 | inline const char* Data() { return data_.c_str(); } 21 | 22 | inline const size_t Size() { return data_size_; } 23 | 24 | inline const size_t HeaderSize() { return header_size_; } 25 | 26 | private: 27 | std::string data_; 28 | size_t data_size_; 29 | size_t header_size_; 30 | }; 31 | 32 | class StaticHTTPService { 33 | public: 34 | explicit StaticHTTPService(const std::string& root_dir, 35 | const std::string& index = "index.html"); 36 | 37 | void RegisterRoutes(HTTPRouter* router); 38 | 39 | void ServeIndex(uWS::HttpResponse* res); 40 | 41 | private: 42 | std::unordered_map> cache_; 43 | }; 44 | 45 | } // namespace rustla2 46 | -------------------------------------------------------------------------------- /api/src/Status.cpp: -------------------------------------------------------------------------------- 1 | #include "Status.h" 2 | 3 | namespace rustla2 { 4 | 5 | const Status& Status::OK = Status(StatusCode::OK, ""); 6 | const Status& Status::ERROR = Status(StatusCode::ERROR, ""); 7 | 8 | void Status::WriteJSON( 9 | rapidjson::Writer* writer) const { 10 | writer->StartObject(); 11 | writer->Key("code"); 12 | writer->Int(code_); 13 | writer->Key("message"); 14 | writer->String(error_message_); 15 | writer->Key("details"); 16 | writer->String(error_details_); 17 | writer->EndObject(); 18 | } 19 | 20 | } // namespace rustla2 21 | -------------------------------------------------------------------------------- /api/src/Status.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace rustla2 { 8 | 9 | enum StatusCode { 10 | UNKNOWN = 0, 11 | OK = 1, 12 | ERROR = 2, 13 | HTTP_ERROR = 3, 14 | JSON_PARSE_ERROR = 4, 15 | JSON_SCHEMA_ERROR = 5, 16 | VALIDATION_ERROR = 6, 17 | API_ERROR = 7, 18 | DB_ENGINE_ERROR = 8, 19 | }; 20 | 21 | class Status { 22 | public: 23 | Status() : code_(StatusCode::UNKNOWN) {} 24 | 25 | Status(StatusCode code, const std::string& error_message) 26 | : code_(code), error_message_(error_message) {} 27 | 28 | Status(StatusCode code, const std::string& error_message, 29 | const std::string& error_details) 30 | : code_(code), 31 | error_message_(error_message), 32 | error_details_(error_details) {} 33 | 34 | static const Status& OK; 35 | static const Status& ERROR; 36 | 37 | bool Ok() const { return code_ == StatusCode::OK; } 38 | 39 | StatusCode GetCode() const { return code_; } 40 | 41 | std::string GetErrorMessage() const { return error_message_; } 42 | 43 | std::string GetErrorDetails() const { return error_details_; } 44 | 45 | void WriteJSON(rapidjson::Writer* writer) const; 46 | 47 | private: 48 | StatusCode code_; 49 | std::string error_message_; 50 | std::string error_details_; 51 | }; 52 | 53 | } // namespace rustla2 54 | -------------------------------------------------------------------------------- /api/src/Strings.cpp: -------------------------------------------------------------------------------- 1 | #include "Strings.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace rustla2 { 8 | 9 | // copied from wikipedia. see: 10 | // https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#C++ 11 | int levenshtein_distance(const std::string &s1, const std::string &s2) { 12 | int s1len = s1.size(); 13 | int s2len = s2.size(); 14 | int column_start = 1; 15 | auto column = new int[s1len + 1]; 16 | std::iota(column + column_start - 1, column + s1len + 1, column_start - 1); 17 | 18 | for (auto x = column_start; x <= s2len; x++) { 19 | column[0] = x; 20 | auto last_diagonal = x - column_start; 21 | for (auto y = column_start; y <= s1len; y++) { 22 | auto old_diagonal = column[y]; 23 | auto possibilities = {column[y] + 1, column[y - 1] + 1, 24 | last_diagonal + (s1[y - 1] == s2[x - 1] ? 0 : 1)}; 25 | column[y] = std::min(possibilities); 26 | last_diagonal = old_diagonal; 27 | } 28 | } 29 | auto result = column[s1len]; 30 | delete[] column; 31 | return result; 32 | } 33 | 34 | } // namespace rustla2 35 | -------------------------------------------------------------------------------- /api/src/Strings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace rustla2 { 6 | 7 | // copied from wikipedia. see: 8 | // https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#C++ 9 | int levenshtein_distance(const std::string &s1, const std::string &s2); 10 | 11 | } // namespace rustla2 12 | -------------------------------------------------------------------------------- /api/src/ViewerStates.cpp: -------------------------------------------------------------------------------- 1 | #include "ViewerStates.h" 2 | 3 | namespace rustla2 { 4 | 5 | void UserState::WriteJSON(rapidjson::Writer *writer) { 6 | writer->StartObject(); 7 | writer->Key("user_id"); 8 | writer->String(user_id); 9 | writer->Key("name"); 10 | writer->String(name); 11 | writer->Key("online"); 12 | writer->Bool(online); 13 | writer->Key("enable_public_state"); 14 | writer->Bool(enable_public_state); 15 | writer->Key("stream_id"); 16 | writer->Uint64(stream_id); 17 | 18 | if (channel != nullptr) { 19 | writer->Key("channel"); 20 | channel->WriteJSON(writer); 21 | } 22 | 23 | writer->EndObject(); 24 | } 25 | 26 | void ViewerStates::IncrViewerStream(const std::string &user_id, 27 | const std::uint64_t stream_id) { 28 | if (user_id.empty()) { 29 | return; 30 | } 31 | 32 | boost::lock_guard lock{lock_}; 33 | 34 | data_[{user_id, stream_id}]++; 35 | 36 | user_change_observers_.Mark(user_id); 37 | } 38 | 39 | void ViewerStates::DecrViewerStream(const std::string &user_id, 40 | const std::uint64_t stream_id) { 41 | if (user_id.empty()) { 42 | return; 43 | } 44 | 45 | boost::lock_guard lock{lock_}; 46 | 47 | auto session_count = --data_[{user_id, stream_id}]; 48 | if (session_count <= 0) { 49 | data_.erase({user_id, stream_id}); 50 | } 51 | 52 | user_change_observers_.Mark(user_id); 53 | } 54 | 55 | std::shared_ptr ViewerStates::CreateObserver() { 56 | boost::lock_guard lock{lock_}; 57 | 58 | std::set ids; 59 | std::transform(data_.begin(), data_.end(), std::inserter(ids, ids.begin()), 60 | [&](auto i) { return i.first.user_id; }); 61 | 62 | return user_change_observers_.CreateObserver(ids); 63 | } 64 | 65 | void ViewerStates::StopObserving( 66 | std::shared_ptr observer) { 67 | user_change_observers_.StopObserving(observer); 68 | } 69 | 70 | void ViewerStates::MarkUserChanged(const std::string &user_id) { 71 | user_change_observers_.Mark(user_id); 72 | } 73 | 74 | bool ViewerStates::GetNextUserState( 75 | std::shared_ptr observer, UserState *state) { 76 | UserState new_state; 77 | 78 | if (!observer->Next(&new_state.user_id)) { 79 | return false; 80 | } 81 | 82 | boost::lock_guard lock{lock_}; 83 | 84 | auto user = users_->GetByID(new_state.user_id); 85 | if (user != nullptr) { 86 | new_state.name = user->GetName(); 87 | new_state.enable_public_state = user->GetEnablePublicState(); 88 | } 89 | 90 | auto user_id_state = data_.upper_bound({new_state.user_id, UINT64_MAX}); 91 | if (user_id_state != data_.begin()) { 92 | user_id_state--; 93 | if (user_id_state->first.user_id == new_state.user_id) { 94 | new_state.stream_id = user_id_state->first.stream_id; 95 | new_state.online = true; 96 | 97 | auto stream = streams_->GetByID(new_state.stream_id); 98 | if (stream != nullptr) { 99 | new_state.channel = stream->GetChannel(); 100 | } 101 | } 102 | } 103 | 104 | *state = new_state; 105 | return true; 106 | } 107 | 108 | } // namespace rustla2 109 | -------------------------------------------------------------------------------- /api/src/ViewerStates.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "Channel.h" 14 | #include "Observer.h" 15 | #include "Streams.h" 16 | #include "Users.h" 17 | 18 | namespace rustla2 { 19 | 20 | struct ViewerKey { 21 | std::string user_id; 22 | std::uint64_t stream_id; 23 | }; 24 | 25 | struct ViewerKeyLess : public std::binary_function { 26 | bool operator()(const ViewerKey &a, const ViewerKey &b) const { 27 | return a.user_id == b.user_id ? a.stream_id < b.stream_id 28 | : a.user_id < b.user_id; 29 | } 30 | }; 31 | 32 | using ViewerMap = std::map; 33 | 34 | struct UserState { 35 | void WriteJSON(rapidjson::Writer *writer); 36 | 37 | std::string user_id{""}; 38 | std::string name{""}; 39 | bool online{false}; 40 | bool enable_public_state{false}; 41 | std::uint64_t stream_id{0}; 42 | std::shared_ptr channel; 43 | }; 44 | 45 | using ViewerStateObserver = Observer; 46 | 47 | class ViewerStates { 48 | public: 49 | explicit ViewerStates(std::shared_ptr users, 50 | std::shared_ptr streams) 51 | : users_(users), streams_(streams){}; 52 | 53 | void IncrViewerStream(const std::string &user_id, 54 | const std::uint64_t stream_id); 55 | 56 | void DecrViewerStream(const std::string &user_id, 57 | const std::uint64_t stream_id); 58 | 59 | std::shared_ptr CreateObserver(); 60 | 61 | void StopObserving(std::shared_ptr observer); 62 | 63 | void MarkUserChanged(const std::string &user_id); 64 | 65 | bool GetNextUserState(std::shared_ptr observer, 66 | UserState *state); 67 | 68 | private: 69 | std::shared_ptr users_; 70 | std::shared_ptr streams_; 71 | boost::mutex lock_; 72 | ViewerMap data_; 73 | Observable user_change_observers_; 74 | }; 75 | 76 | } // namespace rustla2 77 | -------------------------------------------------------------------------------- /api/src/WSService.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "Channel.h" 13 | #include "DB.h" 14 | #include "HTTPRequest.h" 15 | #include "Observer.h" 16 | #include "Streams.h" 17 | 18 | namespace rustla2 { 19 | 20 | struct WSState { 21 | boost::uuids::uuid id; 22 | std::string user_id{""}; 23 | std::string ip{""}; 24 | uint64_t stream_id{0}; 25 | bool afk{false}; 26 | }; 27 | 28 | class WSService { 29 | public: 30 | WSService(std::shared_ptr db, uWS::Hub* hub); 31 | 32 | ~WSService(); 33 | 34 | void SetAFK(uWS::WebSocket* ws, 35 | const rapidjson::Document& input); 36 | 37 | void SetAFK(uWS::WebSocket* ws, const bool afk, 38 | rapidjson::Writer* writer); 39 | 40 | void GetStream(uWS::WebSocket* ws, 41 | const rapidjson::Document& input); 42 | 43 | void GetStreamByID(const uint64_t stream_id, 44 | rapidjson::Writer* writer); 45 | 46 | void SetStream(uWS::WebSocket* ws, 47 | const rapidjson::Document& input); 48 | 49 | inline void SetStreamToChannel( 50 | const std::string& channel, const std::string& service, 51 | rapidjson::Writer* writer, uint64_t* stream_id); 52 | 53 | void SetStreamToStreamPath(const std::string& stream_path, 54 | rapidjson::Writer* writer, 55 | uint64_t* stream_id); 56 | 57 | void SetStreamToChannel(const Channel& channel, 58 | rapidjson::Writer* writer, 59 | uint64_t* stream_id); 60 | 61 | void SetStreamToNull(rapidjson::Writer* writer, 62 | uint64_t* stream_id); 63 | 64 | void UnsetStream(uWS::WebSocket* ws); 65 | 66 | void BroadcastStreams(); 67 | 68 | void BroadcastRustlers(); 69 | 70 | void BroadcastViewerState(); 71 | 72 | std::shared_ptr GetWSStream(uWS::WebSocket* ws); 73 | 74 | WSState* GetWSState(uWS::WebSocket* ws); 75 | 76 | private: 77 | std::shared_ptr db_; 78 | uWS::Hub* hub_; 79 | Timer stream_broadcast_timer_; 80 | Timer rustler_broadcast_timer_; 81 | rapidjson::StringBuffer buf_; 82 | std::shared_ptr> stream_observer_; 83 | std::string last_streams_json_; 84 | }; 85 | 86 | } // namespace rustla2 87 | -------------------------------------------------------------------------------- /api/src/YoutubeClient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "APIClient.h" 8 | #include "Config.h" 9 | #include "Status.h" 10 | 11 | namespace rustla2 { 12 | namespace youtube { 13 | 14 | class VideosResult : public APIResult { 15 | public: 16 | class Video { 17 | public: 18 | explicit Video(const rapidjson::Value& data) : data_(data) {} 19 | 20 | std::string GetTitle() const; 21 | 22 | uint64_t GetViewers() const; 23 | 24 | bool IsNSFW() const; 25 | 26 | std::string GetMediumThumbnail() const; 27 | 28 | private: 29 | const rapidjson::Value& data_; 30 | }; 31 | 32 | rapidjson::Document GetSchema() final; 33 | 34 | bool IsEmpty() const; 35 | 36 | uint64_t GetTotalResults() const; 37 | 38 | const VideosResult::Video GetVideo(const size_t index) const; 39 | }; 40 | 41 | class ErrorResult : public APIResult { 42 | public: 43 | rapidjson::Document GetSchema() final; 44 | 45 | uint64_t GetErrorCode() const; 46 | 47 | std::string GetMessage() const; 48 | }; 49 | 50 | struct ClientConfig { 51 | const std::string public_api_key; 52 | }; 53 | 54 | class Client { 55 | public: 56 | explicit Client(ClientConfig config) : config_(config) {} 57 | 58 | Status GetVideosByID(const std::string& id, VideosResult* result); 59 | 60 | private: 61 | ClientConfig config_; 62 | }; 63 | 64 | } // namespace youtube 65 | } // namespace rustla2 66 | -------------------------------------------------------------------------------- /api/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "Config.h" 12 | #include "DB.h" 13 | #include "HTTPService.h" 14 | #include "ServicePoller.h" 15 | #include "WSService.h" 16 | 17 | DEFINE_uint64(concurrency, 0, "Server thread count (defaults to core count)"); 18 | 19 | namespace rustla2 { 20 | 21 | class Runner { 22 | public: 23 | Runner() : db_(new DB()) {} 24 | 25 | void Run() { 26 | ServicePoller service_poller(db_); 27 | folly::FunctionScheduler scheduler; 28 | scheduler.addFunction( 29 | [&]() { service_poller.Run(); }, 30 | std::chrono::milliseconds(Config::Get().GetLivecheckInterval()), 31 | "ServicePoller"); 32 | scheduler.start(); 33 | 34 | auto concurrency = FLAGS_concurrency ? FLAGS_concurrency 35 | : std::thread::hardware_concurrency(); 36 | LOG(INFO) << "starting " << concurrency << " server thread(s)"; 37 | 38 | std::vector threads(concurrency); 39 | std::transform(threads.begin(), threads.end(), threads.begin(), 40 | [&](std::thread *t) { return CreateThread(); }); 41 | 42 | for (const auto &thread : threads) { 43 | thread->join(); 44 | } 45 | } 46 | 47 | private: 48 | std::thread *CreateThread() { 49 | return new std::thread([&]() { 50 | uWS::Hub hub; 51 | 52 | WSService ws_service(db_, &hub); 53 | HTTPService http_service(db_, &hub); 54 | 55 | if (!Listen(&hub)) { 56 | LOG(FATAL) << "unable to listen"; 57 | } 58 | 59 | hub.run(); 60 | }); 61 | } 62 | 63 | bool Listen(uWS::Hub *hub) { 64 | auto port = Config::Get().GetPort(); 65 | auto ssl_cert_path = Config::Get().GetSSLCertPath(); 66 | auto ssl_key_path = Config::Get().GetSSLKeyPath(); 67 | auto ssl_key_password = Config::Get().GetSSLKeyPassword(); 68 | 69 | if (!ssl_cert_path.empty() && !ssl_key_path.empty()) { 70 | return hub->listen( 71 | port, 72 | uS::TLS::createContext(ssl_cert_path, ssl_key_path, ssl_key_password), 73 | uS::ListenOptions::REUSE_PORT); 74 | } 75 | 76 | return hub->listen(port, nullptr, uS::ListenOptions::REUSE_PORT); 77 | } 78 | 79 | std::shared_ptr db_; 80 | }; 81 | 82 | } // namespace rustla2 83 | 84 | int main(int argc, char **argv) { 85 | google::InitGoogleLogging(argv[0]); 86 | google::ParseCommandLineFlags(&argc, &argv, false); 87 | 88 | rustla2::Config::Get().Init(".env"); 89 | 90 | rustla2::Runner runner; 91 | runner.Run(); 92 | } 93 | -------------------------------------------------------------------------------- /api/tests/AngelthumpTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include "../src/AngelThumpClient.h" 8 | 9 | namespace rustla2 { 10 | 11 | TEST(AngelthumpTest, TestNSFW) { 12 | std::string resp = R"json( 13 | [ 14 | { 15 | "type": "live", 16 | "thumbnail_url": "https://thumbnail.angelthump.com/thumbnails/psrngafk.jpeg", 17 | "viewer_count": "38", 18 | "user": { 19 | "offline_banner_url": "https://images-angelthump.nyc3.cdn.digitaloceanspaces.com/offline-banners/31dc54b09264952f60fcdd6b7b743920be725a74a1026a858b81b259cfc79fc6.png", 20 | "title": "Arrival (2016)", 21 | "nsfw": true 22 | } 23 | } 24 | ] 25 | )json"; 26 | 27 | angelthump::ChannelResult chnl; 28 | auto status = chnl.SetData(resp.c_str(), resp.size()); 29 | if (status.Ok()) { 30 | EXPECT_EQ(chnl.GetViewers(), 38); 31 | EXPECT_STREQ(chnl.GetThumbnail().c_str(), 32 | "https://thumbnail.angelthump.com/thumbnails/psrngafk.jpeg"); 33 | EXPECT_STREQ(chnl.GetTitle().c_str(), "Arrival (2016)"); 34 | EXPECT_TRUE(chnl.IsNSFW()); 35 | } else { 36 | LOG(INFO) << status.GetErrorMessage(); 37 | LOG(INFO) << status.GetErrorDetails(); 38 | FAIL(); 39 | } 40 | } 41 | 42 | TEST(AngelthumpTest, TestNotNSFW) { 43 | std::string resp = R"json( 44 | [ 45 | { 46 | "type": "live", 47 | "thumbnail_url": "https://thumbnail.angelthump.com/thumbnails/psrngafk.jpeg", 48 | "viewer_count": "34", 49 | "user": { 50 | "offline_banner_url": "https://images-angelthump.nyc3.cdn.digitaloceanspaces.com/offline-banners/31dc54b09264952f60fcdd6b7b743920be725a74a1026a858b81b259cfc79fc6.png", 51 | "title": "Arrival (2016)", 52 | "nsfw": false 53 | } 54 | } 55 | ] 56 | )json"; 57 | 58 | angelthump::ChannelResult chnl; 59 | auto status = chnl.SetData(resp.c_str(), resp.size()); 60 | if (status.Ok()) { 61 | EXPECT_EQ(chnl.GetViewers(), 34); 62 | EXPECT_STREQ(chnl.GetThumbnail().c_str(), 63 | "https://thumbnail.angelthump.com/thumbnails/psrngafk.jpeg"); 64 | EXPECT_STREQ(chnl.GetTitle().c_str(), "Arrival (2016)"); 65 | EXPECT_FALSE(chnl.IsNSFW()); 66 | } else { 67 | LOG(INFO) << status.GetErrorMessage(); 68 | LOG(INFO) << status.GetErrorDetails(); 69 | FAIL(); 70 | } 71 | } 72 | 73 | } // namespace rustla2 74 | 75 | int main(int argc, char **argv) { 76 | google::InitGoogleLogging(argv[0]); 77 | google::ParseCommandLineFlags(&argc, &argv, false); 78 | testing::InitGoogleTest(&argc, argv); 79 | 80 | return RUN_ALL_TESTS(); 81 | } 82 | -------------------------------------------------------------------------------- /api/tests/CurlTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../src/Curl.h" 5 | 6 | namespace rustla2 { 7 | 8 | TEST(CurlTest, Test) { 9 | CurlRequest req("https://www.google.com/"); 10 | req.Submit(); 11 | 12 | EXPECT_EQ(req.GetResponseCode(), 200); 13 | } 14 | 15 | } // namespace rustla2 16 | 17 | int main(int argc, char **argv) { 18 | testing::InitGoogleTest(&argc, argv); 19 | 20 | return RUN_ALL_TESTS(); 21 | } 22 | -------------------------------------------------------------------------------- /api/tests/HTTPRouterTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "../src/HTTPRequest.h" 6 | #include "../src/HTTPRouter.h" 7 | 8 | namespace rustla2 { 9 | 10 | namespace { 11 | 12 | constexpr uWS::HttpMethod GET = uWS::HttpMethod::METHOD_GET; 13 | 14 | std::function GetHandler() { 15 | return [](const HTTPRouteHandler &handler) { 16 | uWS::HttpResponse *res = nullptr; 17 | HTTPRequest *req = nullptr; 18 | 19 | handler(res, req); 20 | }; 21 | } 22 | 23 | } // namespace 24 | 25 | TEST(HTTPRouterTest, TestRoot) { 26 | rustla2::HTTPRouter router; 27 | uint64_t count = 0; 28 | 29 | router.Get("/", [&](uWS::HttpResponse *res, HTTPRequest *req) { ++count; }); 30 | 31 | EXPECT_TRUE(router.Dispatch("/", GET, GetHandler())); 32 | EXPECT_FALSE(router.Dispatch("/test", GET, GetHandler())); 33 | 34 | EXPECT_EQ(count, 1); 35 | } 36 | 37 | TEST(HTTPRouterTest, TestWild) { 38 | rustla2::HTTPRouter router; 39 | uint64_t count = 0; 40 | 41 | router.Get("/some/wild/*", 42 | [&](uWS::HttpResponse *res, HTTPRequest *req) { ++count; }); 43 | 44 | EXPECT_TRUE(router.Dispatch("/some/wild/test", GET, GetHandler())); 45 | EXPECT_FALSE(router.Dispatch("/some/wild/test/path", GET, GetHandler())); 46 | EXPECT_FALSE(router.Dispatch("/some/wild", GET, GetHandler())); 47 | 48 | EXPECT_EQ(count, 1); 49 | } 50 | 51 | TEST(HTTPRouterTest, TestWildSubpath) { 52 | rustla2::HTTPRouter router; 53 | uint64_t count = 0; 54 | 55 | router.Get("/some/wild/**", 56 | [&](uWS::HttpResponse *res, HTTPRequest *req) { ++count; }); 57 | 58 | EXPECT_TRUE(router.Dispatch("/some/wild/test", GET, GetHandler())); 59 | EXPECT_TRUE(router.Dispatch("/some/wild/test/path", GET, GetHandler())); 60 | EXPECT_FALSE(router.Dispatch("/some/wild", GET, GetHandler())); 61 | 62 | EXPECT_EQ(count, 2); 63 | } 64 | 65 | } // namespace rustla2 66 | 67 | int main(int argc, char **argv) { 68 | testing::InitGoogleTest(&argc, argv); 69 | 70 | return RUN_ALL_TESTS(); 71 | } 72 | -------------------------------------------------------------------------------- /api/tests/IPRangesTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "../src/IPRanges.h" 7 | 8 | namespace rustla2 { 9 | 10 | TEST(IPRangesTest, TestV4) { 11 | sqlite::database db(":memory:"); 12 | IPRanges test_ranges(db, "ip_ranges"); 13 | 14 | test_ranges.Insert("127.0.0.25", "127.0.0.50"); 15 | test_ranges.Insert("127.0.0.25", "127.0.0.100"); 16 | test_ranges.Insert("127.0.2.1", "127.0.5.1"); 17 | 18 | EXPECT_TRUE(test_ranges.Contains("127.0.0.25")); 19 | EXPECT_TRUE(test_ranges.Contains("127.0.0.30")); 20 | EXPECT_TRUE(test_ranges.Contains("::ffff:127.0.0.30")); 21 | EXPECT_TRUE(test_ranges.Contains("127.0.3.1")); 22 | 23 | EXPECT_FALSE(test_ranges.Contains("127.0.0.10")); 24 | EXPECT_FALSE(test_ranges.Contains("127.1.0.1")); 25 | EXPECT_FALSE(test_ranges.Contains("::ffff:127.1.0.1")); 26 | 27 | EXPECT_FALSE(test_ranges.Contains("2001:0db8:85a3:0000:0000:8a2e:0370:7334")); 28 | } 29 | 30 | TEST(IPRangesTest, TestV6) { 31 | sqlite::database db(":memory:"); 32 | IPRanges test_ranges(db, "ip_ranges"); 33 | 34 | test_ranges.Insert("2001:0000:0000:0000:0000:0000:0000:0000", 35 | "2001:0000:0000:0000:ffff:0000:0000:0000"); 36 | 37 | test_ranges.Insert("2001:0000:0000:0000:0000:0000:0000:0000", 38 | "2001:0000:0000:ffff:0000:0000:0000:0000"); 39 | 40 | EXPECT_TRUE(test_ranges.Contains("2001:0000:0000:0000:0000:0001:0000:0000")); 41 | 42 | EXPECT_FALSE(test_ranges.Contains("2001:0000:0001:0000:0000:0000:0000:0000")); 43 | 44 | EXPECT_FALSE(test_ranges.Contains("127.0.0.10")); 45 | } 46 | 47 | TEST(IPRangesTest, TestInvalidValues) { 48 | sqlite::database db(":memory:"); 49 | IPRanges test_ranges(db, "ip_ranges"); 50 | 51 | EXPECT_FALSE(test_ranges.Contains("some invalid value")); 52 | EXPECT_FALSE(test_ranges.Contains("")); 53 | } 54 | 55 | } // namespace rustla2 56 | 57 | int main(int argc, char **argv) { 58 | google::InitGoogleLogging(argv[0]); 59 | testing::InitGoogleTest(&argc, argv); 60 | 61 | return RUN_ALL_TESTS(); 62 | } 63 | -------------------------------------------------------------------------------- /api/tests/SmashcastTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include "../src/SmashcastClient.h" 8 | 9 | namespace rustla2 { 10 | 11 | TEST(smashcastTest, TestNSFW) { 12 | std::string resp = R"json( 13 | { 14 | "livestream": [ 15 | { 16 | "media_is_live": "1", 17 | "media_status": "Artificial Intelligence Tournament", 18 | "media_views": "128", 19 | "media_thumbnail": "/static/img/media/live/sscaitournament_mid_000.jpg" 20 | } 21 | ] 22 | } 23 | )json"; 24 | 25 | smashcast::ChannelResult chnl; 26 | auto status = chnl.SetData(resp.c_str(), resp.size()); 27 | if (status.Ok()) { 28 | EXPECT_EQ(chnl.GetViewers(), 128); 29 | EXPECT_STREQ(chnl.GetThumbnail().c_str(), 30 | "https://edge.sf.hitbox.tv/static/img/media/live/sscaitournament_mid_000.jpg"); 31 | EXPECT_STREQ(chnl.GetTitle().c_str(), "Artificial Intelligence Tournament"); 32 | EXPECT_TRUE(chnl.GetLive()); 33 | } else { 34 | LOG(INFO) << status.GetErrorMessage(); 35 | LOG(INFO) << status.GetErrorDetails(); 36 | FAIL(); 37 | } 38 | } 39 | 40 | } // namespace rustla2 41 | 42 | int main(int argc, char **argv) { 43 | google::InitGoogleLogging(argv[0]); 44 | google::ParseCommandLineFlags(&argc, &argv, false); 45 | testing::InitGoogleTest(&argc, argv); 46 | 47 | return RUN_ALL_TESTS(); 48 | } 49 | -------------------------------------------------------------------------------- /api/tests/UsersTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "../src/Config.h" 7 | #include "../src/Users.h" 8 | 9 | namespace rustla2 { 10 | 11 | TEST(UsersTest, TestSetName) { 12 | Config::Get().Init("users_test.env"); 13 | 14 | sqlite::database db(":memory:"); 15 | 16 | auto channel = Channel::Create("twitch", "test"); 17 | 18 | auto names = std::vector{"InfiniteJester", "beepybeepy", 19 | "SpiderTechnitian"}; 20 | for (const auto &name : names) { 21 | User user(db, 1, channel, "10.0.0.1"); 22 | auto status = user.SetName(name); 23 | LOG(INFO) << status.GetErrorMessage(); 24 | EXPECT_TRUE(status.Ok()); 25 | } 26 | } 27 | 28 | } // namespace rustla2 29 | 30 | int main(int argc, char **argv) { 31 | google::InitGoogleLogging(argv[0]); 32 | google::ParseCommandLineFlags(&argc, &argv, false); 33 | testing::InitGoogleTest(&argc, argv); 34 | 35 | return RUN_ALL_TESTS(); 36 | } 37 | -------------------------------------------------------------------------------- /api/tests/users_test.env: -------------------------------------------------------------------------------- 1 | TWITCH_REDIRECT_URI=https://localhost:8080/oauth 2 | 3 | EMOTES=Dravewin,INFESTINY,FIDGETLOL,Hhhehhehe,GameOfThrows,WORTH,FeedNathan,Abathur,LUL,Heimerdonger,SoSad,DURRSTINY,SURPRISE,NoTears,OverRustle,DuckerZ,Kappa,Klappa,DappaKappa,BibleThump,AngelThump,FrankerZ,BasedGod,OhKrappa,SoDoge,WhoahDude,MotherFuckinGame,DaFeels,UWOTM8,CallCatz,CallChad,DatGeoff,Disgustiny,FerretLOL,Sippy,DestiSenpaii,Nappa,DAFUK,AYYYLMAO,DANKMEMES,MLADY,SOTRIGGERED,MASTERB8,NOTMYTEMPO,LIES,LeRuse,YEE,SWEATSTINY,PEPE,CheekerZ,SpookerZ,SLEEPSTINY,PICNIC,Memegasm,WEEWOO,KappaRoss,ASLAN,DJAslan,TRUMPED,BASEDWATM8,BERN,HmmStiny,PepoThink,FeelsAmazingMan,FeelsBadMan,FeelsGoodMan,OhMyDog,Wowee,haHAA,POTATO,NOBULLY,ChibiDesti,gachiGASM,SUESTINY,GODSTINY,REE,Blubstiny,monkaS,Depresstiny,Shekels,RaveDoge,CuckCrab,SourPls,ECH,AUTISTINY,MingLee,DESBRO,TooSpicy,CARBUCKS,ChanChamp,Jewstiny,NiceMeMe,CUX,DSPstiny,ITSRAWWW,Riperino,TopCake,WEOW,4Head,AlisherZ,BabyRage,DansGame,dayJoy,DatSheffy,EleGiggle,kaceyFace,Keepo,Kreygasm,lirikThump,OpieOP,PJSalt,PogChamp,ResidentSleeper,SMOrc,SSSsss,SwiftRage,WinWaker,NotLikeThis 4 | 5 | EMOTE_SIMILARITY_MIN_LENGTH=5 6 | -------------------------------------------------------------------------------- /config/scala-api-backend.conf: -------------------------------------------------------------------------------- 1 | proxy_pass http://localhost:3002; 2 | proxy_set_header Upgrade $http_upgrade; 3 | proxy_set_header Connection keep-alive; 4 | proxy_set_header Host $host; 5 | proxy_cache_bypass $http_upgrade; 6 | -------------------------------------------------------------------------------- /config/strims-backend.conf: -------------------------------------------------------------------------------- 1 | proxy_pass http://localhost:8076; 2 | proxy_set_header Upgrade $http_upgrade; 3 | proxy_set_header Connection keep-alive; 4 | proxy_set_header Host $host; 5 | proxy_cache_bypass $http_upgrade; 6 | 7 | -------------------------------------------------------------------------------- /config/strims-security-common.conf: -------------------------------------------------------------------------------- 1 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; 2 | add_header X-Content-Type-Options nosniff; 3 | add_header Feature-Policy "geolocation 'none'; microphone 'none'; camera 'none'"; 4 | add_header Content-Security-Policy "default-src 'none'; connect-src 'self' https: wss:; frame-src https:; img-src 'self' https:; script-src 'self' 'unsafe-eval' blob:; media-src blob: data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"; 5 | add_header X-Robots-Tag: none; 6 | -------------------------------------------------------------------------------- /config/strims.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name strims.gg; 4 | 5 | # XXX: make sure these are only enabled if we are actually using CF. 6 | set_real_ip_from 0.0.0.0/0; 7 | real_ip_header CF-Connecting-IP; 8 | 9 | access_log /var/log/nginx/access_strims.log; 10 | error_log /var/log/nginx/error_strims.log; 11 | 12 | include strims-security-common.conf; 13 | add_header Referrer-Policy "same-origin"; 14 | 15 | ssl_certificate /etc/letsencrypt/live/strims.gg/fullchain.pem; 16 | ssl_certificate_key /etc/letsencrypt/live/strims.gg/privkey.pem; 17 | 18 | # Rustla2 backend 19 | location / { 20 | include strims-backend.conf; 21 | } 22 | 23 | # allow anyone to read streamer info - mostly used for dev purposes 24 | location /api/streamer/ { 25 | add_header Access-Control-Allow-Origin "*"; 26 | include strims-security-common.conf; 27 | include strims-backend.conf; 28 | } 29 | 30 | # Scala-API backend 31 | location /scala-api { 32 | rewrite /scala-api/(.*) /$1 break; 33 | include scala-api-backend.conf; 34 | } 35 | 36 | location /youtube/ { 37 | include strims-backend.conf; 38 | 39 | # youtube chat requires a referrer to be set 40 | add_header Referrer-Policy "strict-origin-when-cross-origin"; 41 | # adding headers in nested location required to re-add everything 42 | include strims-security-common.conf; 43 | } 44 | } 45 | 46 | server { 47 | listen 80; 48 | server_name strims.gg; 49 | return 302 https://$host$request_uri; 50 | } 51 | -------------------------------------------------------------------------------- /emote-update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | emotes=$(curl https://raw.githubusercontent.com/memelabs/chat-gui/master/assets/emotes.json | jq '.default | .[]') 3 | list="" 4 | for e in $emotes; do 5 | list+=$(echo $e | sed -e 's/^"//' -e 's/"$//'), 6 | done 7 | 8 | sed -ie "s/^EMOTES=.*/EMOTES=$(echo $list | rev | cut -c 2- | rev)/" .env.example 9 | -------------------------------------------------------------------------------- /flow-typed/npm/@babel/polyfill_v7.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 70ae65c95b3420e3469c9c30171c1835 2 | // flow-typed version: c6154227d1/@babel/polyfill_v7.x.x/flow_>=v0.104.x 3 | 4 | declare module '@babel/polyfill' {} 5 | -------------------------------------------------------------------------------- /flow-typed/npm/classnames_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: a00cf41b09af4862583460529d5cfcb9 2 | // flow-typed version: c6154227d1/classnames_v2.x.x/flow_>=v0.104.x 3 | 4 | type $npm$classnames$Classes = 5 | | string 6 | | { [className: string]: *, ... } 7 | | false 8 | | void 9 | | null; 10 | 11 | declare module "classnames" { 12 | declare module.exports: ( 13 | ...classes: Array<$npm$classnames$Classes | $npm$classnames$Classes[]> 14 | ) => string; 15 | } 16 | 17 | declare module "classnames/bind" { 18 | declare module.exports: $Exports<"classnames">; 19 | } 20 | 21 | declare module "classnames/dedupe" { 22 | declare module.exports: $Exports<"classnames">; 23 | } 24 | -------------------------------------------------------------------------------- /flow-typed/npm/dotenv_v8.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: f4a700556e8a1a83c5e3ae513140d88c 2 | // flow-typed version: 9899c09849/dotenv_v8.x.x/flow_>=v0.53.x 3 | 4 | // @flow 5 | 6 | declare module 'dotenv' { 7 | declare type ParseResult = {| [key: string]: string |}; 8 | declare type ConfigResult = {| parsed: ParseResult |} | {| error: Error |}; 9 | declare function config( 10 | options?: $Shape<{| path: string, encoding: string, debug: boolean |}> 11 | ): ConfigResult; 12 | 13 | declare function parse( 14 | buffer: Buffer | string, 15 | options?: $Shape<{| debug: boolean |}> 16 | ): ParseResult; 17 | 18 | declare module.exports: {| 19 | config: typeof config, 20 | parse: typeof parse, 21 | |}; 22 | } 23 | 24 | // eslint-disable-next-line no-empty 25 | declare module 'dotenv/config' { 26 | } 27 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 28fdff7f110e1c75efab63ff205dda30 2 | // flow-typed version: c6154227d1/flow-bin_v0.x.x/flow_>=v0.104.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /flow-typed/npm/idx_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 50ba22cd80c7da41ef603aaae19f8b57 2 | // flow-typed version: c6154227d1/idx_v2.x.x/flow_>=v0.104.x 3 | 4 | // From: https://github.com/facebookincubator/idx/blob/master/packages/idx/src/idx.js.flow 5 | 6 | declare module idx { 7 | declare module.exports: $Facebookism$Idx; 8 | } 9 | -------------------------------------------------------------------------------- /flow-typed/npm/isomorphic-fetch_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 03bcd2195d27d9c7b8ea57265f6673cd 2 | // flow-typed version: c6154227d1/isomorphic-fetch_v2.x.x/flow_>=v0.104.x 3 | 4 | declare module "isomorphic-fetch" { 5 | declare module.exports: ( 6 | input: string | Request | URL, 7 | init?: RequestOptions 8 | ) => Promise; 9 | } 10 | -------------------------------------------------------------------------------- /flow-typed/npm/prop-types_v15.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: c2be98acf88fa4c9915a337a66702bb5 2 | // flow-typed version: c6154227d1/prop-types_v15.x.x/flow_>=v0.104.x 3 | 4 | type $npm$propTypes$ReactPropsCheckType = ( 5 | props: any, 6 | propName: string, 7 | componentName: string, 8 | href?: string 9 | ) => ?Error; 10 | 11 | // Copied from: https://github.com/facebook/flow/blob/0938da8d7293d0077fbe95c3a3e0eebadb57b012/lib/react.js#L433-L449 12 | declare module 'prop-types' { 13 | declare var array: React$PropType$Primitive>; 14 | declare var bool: React$PropType$Primitive; 15 | declare var func: React$PropType$Primitive<(...a: Array) => mixed>; 16 | declare var number: React$PropType$Primitive; 17 | declare var object: React$PropType$Primitive<{ +[string]: mixed, ... }>; 18 | declare var string: React$PropType$Primitive; 19 | declare var symbol: React$PropType$Primitive; 20 | declare var any: React$PropType$Primitive; 21 | declare var arrayOf: React$PropType$ArrayOf; 22 | declare var element: React$PropType$Primitive; 23 | declare var instanceOf: React$PropType$InstanceOf; 24 | declare var node: React$PropType$Primitive; 25 | declare var objectOf: React$PropType$ObjectOf; 26 | declare var oneOf: React$PropType$OneOf; 27 | declare var oneOfType: React$PropType$OneOfType; 28 | declare var shape: React$PropType$Shape; 29 | 30 | declare function checkPropTypes( 31 | propTypes: { [key: $Keys]: $npm$propTypes$ReactPropsCheckType, ... }, 32 | values: V, 33 | location: string, 34 | componentName: string, 35 | getStack: ?() => ?string 36 | ): void; 37 | } 38 | -------------------------------------------------------------------------------- /flow-typed/npm/qs_v6.9.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 41a61430cdeb84145dd74af3dd90d802 2 | // flow-typed version: 5a7f14d011/qs_v6.9.x/flow_>=v0.104.x 3 | 4 | declare module "qs" { 5 | declare type Charset = 6 | | 'iso-8859-1' 7 | | 'utf-8' 8 | 9 | declare type ParseOptions = {| 10 | allowDots?: boolean, 11 | allowPrototypes?: boolean, 12 | arrayLimit?: number, 13 | charset?: Charset, 14 | charsetSentinel?: boolean, 15 | comma?: boolean, 16 | decoder?: string => mixed, 17 | delimiter?: string, 18 | depth?: number, 19 | ignoreQueryPrefix?: boolean, 20 | interpretNumericEntities?: boolean, 21 | parameterLimit?: number, 22 | parseArrays?: boolean, 23 | plainObjects?: boolean, 24 | strictNullHandling?: boolean, 25 | |}; 26 | 27 | declare type ArrayFormat = "brackets" | "comma" | "indices" | "repeat"; 28 | 29 | declare type FilterFunction = (prefix: string, value: mixed) => mixed; 30 | declare type FilterArray = Array; 31 | declare type Filter = FilterArray | FilterFunction; 32 | 33 | declare type StringifyOptions = {| 34 | addQueryPrefix?: boolean, 35 | allowDots?: boolean, 36 | arrayFormat?: ArrayFormat, 37 | charset?: Charset, 38 | charsetSentinel?: boolean, 39 | delimiter?: string, 40 | encode?: boolean, 41 | encodeValuesOnly?: boolean, 42 | encoder?: mixed => string, 43 | filter?: Filter, 44 | format?: string, 45 | indices?: boolean, 46 | serializeDate?: Function, 47 | skipNulls?: boolean, 48 | sort?: (string, string) => -1 | 0 | 1, 49 | strictNullHandling?: boolean, 50 | |}; 51 | 52 | declare type Formatter = (any) => string; 53 | 54 | declare type Formats = { 55 | RFC1738: string, 56 | RFC3986: string, 57 | "default": string, 58 | formatters: { 59 | RFC1738: Formatter, 60 | RFC3986: Formatter, 61 | ... 62 | }, 63 | ... 64 | }; 65 | 66 | declare module.exports: {| 67 | parse(str: string, opts?: ParseOptions): {[string]: mixed, ...}, 68 | stringify(obj: {[string]: mixed, ...} | Array, opts?: StringifyOptions): string, 69 | formats: Formats, 70 | |}; 71 | } 72 | -------------------------------------------------------------------------------- /flow-typed/npm/react-custom-scrollbars_v4.2.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 0a3052b8342e14734fba6cc9c06ea56d 2 | // flow-typed version: c6154227d1/react-custom-scrollbars_v4.2.x/flow_>=v0.104.x 3 | 4 | declare module "react-custom-scrollbars" { 5 | declare type PositionValues = { 6 | top: number, 7 | left: number, 8 | clientWidth: number, 9 | clientHeight: number, 10 | scrollWidth: number, 11 | scrollHeight: number, 12 | scrollLeft: number, 13 | scrollTop: number, 14 | ... 15 | }; 16 | 17 | declare type Props = { 18 | onScroll?: (event: SyntheticUIEvent<*>) => void, 19 | onScrollFrame?: (values: PositionValues) => void, 20 | onScrollStart?: () => void, 21 | onScrollStop?: () => void, 22 | onUpdate?: (values: PositionValues) => void, 23 | renderView?: React$StatelessFunctionalComponent, 24 | renderTrackHorizontal?: React$StatelessFunctionalComponent, 25 | renderTrackVertical?: React$StatelessFunctionalComponent, 26 | renderThumbHorizontal?: React$StatelessFunctionalComponent, 27 | renderThumbVertical?: React$StatelessFunctionalComponent, 28 | hideTracksWhenNotNeeded?: boolean, 29 | autoHide?: boolean, 30 | autoHideTimeout?: number, 31 | autoHideDuration?: number, 32 | thumbSize?: number, 33 | thumbMinSize?: number, 34 | universal?: boolean, 35 | autoHeight?: boolean, 36 | autoHeightMin?: number | string, 37 | autoHeightMax?: number | string, 38 | ... 39 | }; 40 | 41 | declare export default class Scrollbars extends React$Component { 42 | scrollTop(top: number): void; 43 | scrollLeft(left: number): void; 44 | scrollToTop(): void; 45 | scrollToBottom(): void; 46 | scrollToLeft(): void; 47 | scrollToRight(): void; 48 | getScrollLeft(): number; 49 | getScrollTop(): number; 50 | getScrollWidth(): number; 51 | getScrollHeight(): number; 52 | getClientWidth(): number; 53 | getClientHeight(): number; 54 | getValues(): PositionValues; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /flow-typed/npm/react-loadable_v5.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 835cb38ea2a30f3c659cd6fac5b33242 2 | // flow-typed version: c6154227d1/react-loadable_v5.x.x/flow_>=v0.104.x 3 | 4 | declare type $Await> = T; 5 | 6 | declare module 'react-loadable' { 7 | declare type LoadingProps = { 8 | isLoading: boolean, 9 | pastDelay: boolean, 10 | timedOut: boolean, 11 | retry: () => void, 12 | error: ?Error, 13 | ... 14 | }; 15 | 16 | declare type CommonOptions = { 17 | loading: React$ComponentType, 18 | delay?: number, 19 | timeout?: number, 20 | modules?: Array, 21 | webpack?: () => Array, 22 | ... 23 | }; 24 | 25 | declare type OptionsWithoutRender = { 26 | ...CommonOptions, 27 | loader(): Promise | { default: React$ComponentType, ... }>, 28 | ... 29 | }; 30 | 31 | declare type OptionsWithRender = { 32 | ...CommonOptions, 33 | loader(): Promise, 34 | render(loaded: TModule, props: TProps): React$Node, 35 | ... 36 | }; 37 | 38 | declare type Options = OptionsWithoutRender | OptionsWithRender; 39 | 40 | declare type MapOptions = { 41 | ...CommonOptions, 42 | loader: { [key: $Keys]: () => Promise<*>, ... }, 43 | render(loaded: TModules, props: TProps): React$Node, 44 | ... 45 | }; 46 | 47 | declare class LoadableComponent extends React$Component { 48 | static preload(): Promise 49 | } 50 | 51 | declare type CaptureProps = { report(moduleName: string): void, ... }; 52 | 53 | /** 54 | * A type level function like 55 | * ({ [string]: () => Promise }) -> ({ [string]: T }) 56 | * It would be helpful to apply type arguments to Loadable.Map<> like below. 57 | * 58 | * Loadable.Map import("a") }>({...}); 59 | */ 60 | declare type MapModules = $ObjMap(P) => $Await<*, $Call

>>; 61 | 62 | declare module.exports: { 63 | (opts: Options): Class>, 64 | Map(opts: MapOptions): Class>, 65 | Capture: React$ComponentType, 66 | preloadAll(): Promise, 67 | preloadReady(): Promise, 68 | ... 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /flow-typed/npm/redux_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: f62df6dbce399d55b0f2954c5ac1bd4e 2 | // flow-typed version: c6154227d1/redux_v4.x.x/flow_>=v0.104.x 3 | 4 | declare module 'redux' { 5 | /* 6 | 7 | S = State 8 | A = Action 9 | D = Dispatch 10 | 11 | */ 12 | 13 | declare export type Action = { type: T, ... } 14 | 15 | declare export type DispatchAPI = (action: A) => A; 16 | 17 | declare export type Dispatch = DispatchAPI; 18 | 19 | declare export type MiddlewareAPI> = { 20 | dispatch: D, 21 | getState(): S, 22 | ... 23 | }; 24 | 25 | declare export type Store> = { 26 | // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) 27 | dispatch: D, 28 | getState(): S, 29 | subscribe(listener: () => void): () => void, 30 | replaceReducer(nextReducer: Reducer): void, 31 | ... 32 | }; 33 | 34 | declare export type Reducer = (state: S | void, action: A) => S; 35 | 36 | declare export type CombinedReducer = ( 37 | state: ($Shape & {...}) | void, 38 | action: A 39 | ) => S; 40 | 41 | declare export type Middleware> = ( 42 | api: MiddlewareAPI 43 | ) => (next: D) => D; 44 | 45 | declare export type StoreCreator> = { 46 | (reducer: Reducer, enhancer?: StoreEnhancer): Store, 47 | ( 48 | reducer: Reducer, 49 | preloadedState: S, 50 | enhancer?: StoreEnhancer 51 | ): Store, 52 | ... 53 | }; 54 | 55 | declare export type StoreEnhancer> = ( 56 | next: StoreCreator 57 | ) => StoreCreator; 58 | 59 | declare export function createStore( 60 | reducer: Reducer, 61 | enhancer?: StoreEnhancer 62 | ): Store; 63 | declare export function createStore( 64 | reducer: Reducer, 65 | preloadedState?: S, 66 | enhancer?: StoreEnhancer 67 | ): Store; 68 | 69 | declare export function applyMiddleware( 70 | ...middlewares: Array> 71 | ): StoreEnhancer; 72 | 73 | declare export type ActionCreator = (...args: Array) => A; 74 | declare export type ActionCreators = { [key: K]: ActionCreator, ... }; 75 | 76 | declare export function bindActionCreators< 77 | A, 78 | C: ActionCreator, 79 | D: DispatchAPI 80 | >( 81 | actionCreator: C, 82 | dispatch: D 83 | ): C; 84 | declare export function bindActionCreators< 85 | A, 86 | K, 87 | C: ActionCreators, 88 | D: DispatchAPI 89 | >( 90 | actionCreators: C, 91 | dispatch: D 92 | ): C; 93 | 94 | declare export function combineReducers( 95 | reducers: O 96 | ): CombinedReducer<$ObjMap(r: Reducer) => S>, A>; 97 | 98 | declare export var compose: $Compose; 99 | } 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overrustle", 3 | "version": "1.0.0", 4 | "description": "OverRustle", 5 | "private": true, 6 | "engines": { 7 | "node": ">=12" 8 | }, 9 | "scripts": { 10 | "clean": "rm -rf public/assets public/index.html", 11 | "build": "npm run clean && npm run build:bundle", 12 | "build:bundle": "webpack --progress", 13 | "build:production": "npm run --prod build", 14 | "dev-server": "npm run build && webpack-dev-server --inline --hot", 15 | "flow": "flow check", 16 | "lint": "eslint webpack.config.js --ext .js --ext .jsx src" 17 | }, 18 | "dependencies": { 19 | "@fortawesome/fontawesome-svg-core": "^1.2.30", 20 | "@fortawesome/free-brands-svg-icons": "^5.11.2", 21 | "@fortawesome/free-solid-svg-icons": "^5.11.2", 22 | "@fortawesome/react-fontawesome": "^0.1.7", 23 | "bluebird": "3.7.2", 24 | "bootstrap": "^4.3.1", 25 | "browser-cookies": "^1.1.0", 26 | "clappr": "0.3.11", 27 | "classnames": "^2.2.5", 28 | "dotenv": "8.2.0", 29 | "glyphicons-halflings": "^1.9.1", 30 | "history": "^5.0.0", 31 | "idx": "^2.5.6", 32 | "isomorphic-fetch": "^2.2.1", 33 | "level-selector": "^0.2.0", 34 | "lodash": "^4.17.20", 35 | "prop-types": "^15.7.2", 36 | "qs": "6.9.1", 37 | "react": "^16.8.4", 38 | "react-bootstrap": "1.3.0", 39 | "react-custom-scrollbars": "^4.2.1", 40 | "react-dom": "^16.13.0", 41 | "react-draggable": "^2.2.3", 42 | "react-idle-timer": "^4.0.9", 43 | "react-loadable": "^5.3.1", 44 | "react-redux": "^7.2.1", 45 | "react-router-dom": "^4.1.1", 46 | "react-use": "15.3.3", 47 | "recompose": "^0.30.0", 48 | "reconnecting-websocket": "^4.4.0", 49 | "redux": "4.0.5", 50 | "redux-logger": "^3.0.1", 51 | "redux-thunk": "^2.1.0" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "7.11.6", 55 | "@babel/eslint-parser": "7.11.5", 56 | "@babel/plugin-proposal-class-properties": "7.10.4", 57 | "@babel/polyfill": "7.11.5", 58 | "@babel/preset-env": "7.11.5", 59 | "@babel/preset-flow": "7.10.4", 60 | "@babel/preset-react": "7.10.4", 61 | "autoprefixer": "^9.8.6", 62 | "babel-loader": "8.1.0", 63 | "babel-plugin-idx": "2.4.0", 64 | "babel-plugin-transform-react-remove-prop-types": "0.4.24", 65 | "css-loader": "^3.1.0", 66 | "css-module-flow": "^1.0.0", 67 | "eslint": "7.7.0", 68 | "eslint-loader": "4.0.2", 69 | "eslint-plugin-flowtype": "4.5.2", 70 | "eslint-plugin-react": "7.20.6", 71 | "file-loader": "^2.0.0", 72 | "flow-bin": "0.110.0", 73 | "helper-git-hash": "^1.0.0", 74 | "html-webpack-exclude-assets-plugin": "0.0.7", 75 | "html-webpack-plugin": "^3.1.0", 76 | "lodash-webpack-plugin": "^0.11.5", 77 | "mini-css-extract-plugin": "0.8.0", 78 | "node-sass": "5.0.0", 79 | "postcss-loader": "^3.0.0", 80 | "sass-loader": "10.1.1", 81 | "style-loader": "1.0.0", 82 | "terser-webpack-plugin": "4.2.2", 83 | "url-loader": "2.3.0", 84 | "webpack": "4.44.1", 85 | "webpack-cli": "3.3.12", 86 | "webpack-dev-server": "3.11.0", 87 | "webpack-subresource-integrity": "1.3.4" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /public/image/angelthump.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MemeLabs/Rustla2/ec90bcf6298b52af66ea3b75873a7625c6563532/public/image/angelthump.png -------------------------------------------------------------------------------- /public/image/beand.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MemeLabs/Rustla2/ec90bcf6298b52af66ea3b75873a7625c6563532/public/image/beand.jpg -------------------------------------------------------------------------------- /public/image/donger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MemeLabs/Rustla2/ec90bcf6298b52af66ea3b75873a7625c6563532/public/image/donger.png -------------------------------------------------------------------------------- /public/image/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MemeLabs/Rustla2/ec90bcf6298b52af66ea3b75873a7625c6563532/public/image/favicon.ico -------------------------------------------------------------------------------- /public/image/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MemeLabs/Rustla2/ec90bcf6298b52af66ea3b75873a7625c6563532/public/image/favicon.png -------------------------------------------------------------------------------- /public/image/jigglymonkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MemeLabs/Rustla2/ec90bcf6298b52af66ea3b75873a7625c6563532/public/image/jigglymonkey.png -------------------------------------------------------------------------------- /public/image/pepos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MemeLabs/Rustla2/ec90bcf6298b52af66ea3b75873a7625c6563532/public/image/pepos.png -------------------------------------------------------------------------------- /public/image/twitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MemeLabs/Rustla2/ec90bcf6298b52af66ea3b75873a7625c6563532/public/image/twitch.png -------------------------------------------------------------------------------- /public/image/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MemeLabs/Rustla2/ec90bcf6298b52af66ea3b75873a7625c6563532/public/image/youtube.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | 4 | # Google gets to read the X-Robots headers everywhere. wowee. 5 | User-agent: Googlebot 6 | Allow: / 7 | 8 | -------------------------------------------------------------------------------- /src/INITIAL_STATE.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { CHAT_HOST_STRIMS } from './actions'; 4 | 5 | export default { 6 | isLoading: true, 7 | isAfk: false, 8 | polls: {}, 9 | stream: null, 10 | streams: {}, 11 | ui: { 12 | chatHost: CHAT_HOST_STRIMS, 13 | chatSize: localStorage ? Number(localStorage.getItem('chatSize')) || 400 : 400, 14 | showChat: localStorage ? !(localStorage.getItem('showChat') === 'false') : true, 15 | showHeader: true, 16 | showFooter: true, 17 | }, 18 | self: { 19 | isLoggedIn: false, 20 | profile: { 21 | err: null, 22 | isFetching: false, 23 | data: null, 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/actions/postmessage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* global process */ 4 | 5 | import type { Store } from 'redux'; 6 | 7 | import history from '../history'; 8 | 9 | // thunks for these payloads 10 | const thunks = { 11 | 'STREAM_SET': ({path, channel, service}) => () => { 12 | history.push(path ? `/${path}` : `/${service}/${channel}`); 13 | }, 14 | }; 15 | 16 | export const init = (store: Store<*, *, *>) => { 17 | const handleMessage = (event) => { 18 | const origin = new URL(event.origin); 19 | if (!origin.hostname.endsWith(location.hostname)) { 20 | return; 21 | } 22 | 23 | if (process.env.NODE_ENV !== 'production') { 24 | // eslint-disable-next-line no-console 25 | console.log('window message', event.origin, event.data); 26 | } 27 | 28 | if (thunks[event.data.action]) { 29 | store.dispatch(thunks[event.data.action](event.data.payload)); 30 | } 31 | }; 32 | 33 | window.addEventListener('message', handleMessage, false); 34 | }; 35 | -------------------------------------------------------------------------------- /src/client.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import App from './components/App'; 5 | import store from './store'; 6 | 7 | 8 | const mountPoint = document.getElementById('main'); 9 | render( 10 | , 11 | mountPoint, 12 | () => mountPoint.className = mountPoint.className.replace('jiggle-background', '') 13 | ); 14 | -------------------------------------------------------------------------------- /src/components/AdvancedStreamEmbed.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | 5 | type Props = { 6 | channel: string 7 | }; 8 | 9 | const AdvancedStreamEmbed = ({ channel }: Props) => 10 |