├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── node-backend-docker.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── angular.json ├── apps ├── core-e2e │ ├── .eslintrc.json │ ├── cypress.json │ ├── src │ │ ├── fixtures │ │ │ └── example.json │ │ ├── integration │ │ │ └── app.spec.ts │ │ ├── plugins │ │ │ └── index.js │ │ └── support │ │ │ ├── app.po.ts │ │ │ ├── commands.ts │ │ │ └── index.ts │ ├── tsconfig.e2e.json │ └── tsconfig.json ├── core │ ├── .browserslistrc │ ├── .eslintrc.json │ ├── jest.config.js │ ├── proxy.conf.json │ ├── src │ │ ├── app │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── common │ │ │ │ ├── analytics.service.ts │ │ │ │ ├── emoji.spec.ts │ │ │ │ ├── emoji.ts │ │ │ │ ├── filter.ts │ │ │ │ └── index.ts │ │ │ ├── config │ │ │ │ ├── config.spec.ts │ │ │ │ ├── config.ts │ │ │ │ ├── index.ts │ │ │ │ └── merge-query.ts │ │ │ ├── pages │ │ │ │ ├── comment │ │ │ │ │ ├── comment.module.ts │ │ │ │ │ └── comment.page.ts │ │ │ │ └── home │ │ │ │ │ ├── home.module.ts │ │ │ │ │ ├── home.page.html │ │ │ │ │ ├── home.page.scss │ │ │ │ │ └── home.page.ts │ │ │ └── sources │ │ │ │ ├── acfun.ts │ │ │ │ ├── bilibili.ts │ │ │ │ ├── index.ts │ │ │ │ ├── source.module.ts │ │ │ │ └── source.ts │ │ ├── assets │ │ │ ├── bilichat_icon.png │ │ │ └── css4obs │ │ │ │ └── index.html │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.editor.json │ ├── tsconfig.json │ └── tsconfig.spec.json └── node-backend │ ├── .eslintrc.json │ ├── Dockerfile │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ └── main.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── decorate-angular-cli.js ├── jest.config.js ├── jest.preset.js ├── libs ├── backend-core │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── backend-core.module.ts │ │ │ ├── controllers │ │ │ ├── acfun.controller.ts │ │ │ └── bilibili.controller.ts │ │ │ ├── entry.ts │ │ │ └── services │ │ │ └── bili-user.service.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── common │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── filter.ts │ │ │ ├── message.ts │ │ │ ├── rx.ts │ │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── gamma │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── animations.ts │ │ │ ├── consts.ts │ │ │ ├── gamma-config.service.ts │ │ │ ├── gamma.app.css │ │ │ ├── gamma.app.html │ │ │ ├── gamma.app.ts │ │ │ ├── gamma.module.ts │ │ │ ├── membership-item │ │ │ │ ├── membership-item.html │ │ │ │ └── membership-item.ts │ │ │ ├── message-provider.ts │ │ │ ├── paid-message │ │ │ │ ├── paid-message.html │ │ │ │ └── paid-message.ts │ │ │ ├── paid-sticker │ │ │ │ ├── paid-sticker.html │ │ │ │ └── paid-sticker.ts │ │ │ ├── text-message │ │ │ │ ├── text-message.html │ │ │ │ └── text-message.ts │ │ │ ├── ticker-paid-item │ │ │ │ ├── ticker-paid-item.html │ │ │ │ └── ticker-paid-item.ts │ │ │ ├── ticker-paid-sticker │ │ │ │ ├── ticker-paid-sticker.html │ │ │ │ └── ticker-paid-sticker.ts │ │ │ └── ticker-sponsor-item │ │ │ │ ├── ticker-sponsor-item.html │ │ │ │ └── ticker-sponsor-item.ts │ │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── isomorphic-danmaku-server │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── acfun_prefetch.ts │ │ │ ├── bilibili_prefetch.ts │ │ │ └── types.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── isomorphic-danmaku │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── acfun │ │ │ │ ├── consts.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models │ │ │ │ │ ├── AccessPoint.ts │ │ │ │ │ ├── AccessPointsConfig.ts │ │ │ │ │ ├── AppInfo.ts │ │ │ │ │ ├── AuthorChatPlayerInfo.ts │ │ │ │ │ ├── ChatMediaType.ts │ │ │ │ │ ├── CommonActionSignalComment.ts │ │ │ │ │ ├── CommonActionSignalGift.ts │ │ │ │ │ ├── CommonActionSignalLike.ts │ │ │ │ │ ├── CommonActionSignalRichText.ts │ │ │ │ │ ├── CommonActionSignalUserEnterRoom.ts │ │ │ │ │ ├── CommonActionSignalUserFollowAuthor.ts │ │ │ │ │ ├── CommonNotifySignalCoverAuditResult.ts │ │ │ │ │ ├── CommonNotifySignalKickedOut.ts │ │ │ │ │ ├── CommonNotifySignalLiveManagerState.ts │ │ │ │ │ ├── CommonNotifySignalViolationAlert.ts │ │ │ │ │ ├── CommonStateSignalAuthorChatAccept.ts │ │ │ │ │ ├── CommonStateSignalAuthorChatCall.ts │ │ │ │ │ ├── CommonStateSignalAuthorChatChangeSoundConfig.ts │ │ │ │ │ ├── CommonStateSignalAuthorChatEnd.ts │ │ │ │ │ ├── CommonStateSignalAuthorChatReady.ts │ │ │ │ │ ├── CommonStateSignalChatAccept.ts │ │ │ │ │ ├── CommonStateSignalChatCall.ts │ │ │ │ │ ├── CommonStateSignalChatEnd.ts │ │ │ │ │ ├── CommonStateSignalChatReady.ts │ │ │ │ │ ├── CommonStateSignalCurrentRedpackList.ts │ │ │ │ │ ├── CommonStateSignalDisplayInfo.ts │ │ │ │ │ ├── CommonStateSignalFeatureStateSync.ts │ │ │ │ │ ├── CommonStateSignalPKAccept.ts │ │ │ │ │ ├── CommonStateSignalPKInvitation.ts │ │ │ │ │ ├── CommonStateSignalPKReady.ts │ │ │ │ │ ├── CommonStateSignalPKSoundConfigChanged.ts │ │ │ │ │ ├── CommonStateSignalPkEnd.ts │ │ │ │ │ ├── CommonStateSignalPkStatistic.ts │ │ │ │ │ ├── CommonStateSignalRecentComment.ts │ │ │ │ │ ├── CommonStateSignalTopUsers.ts │ │ │ │ │ ├── CommonStateSignalWishSheetCurrentState.ts │ │ │ │ │ ├── CsAckErrorCode.ts │ │ │ │ │ ├── DeviceInfo.ts │ │ │ │ │ ├── DownstreamPayload.ts │ │ │ │ │ ├── EnvInfo.ts │ │ │ │ │ ├── ErrorMessage.ts │ │ │ │ │ ├── FrontendInfo.ts │ │ │ │ │ ├── I18nCopyWriting.ts │ │ │ │ │ ├── ImageCdnNode.ts │ │ │ │ │ ├── KeepAlive.ts │ │ │ │ │ ├── LinkErrorCode.ts │ │ │ │ │ ├── LiveFeatureState.ts │ │ │ │ │ ├── LocaleMessage.ts │ │ │ │ │ ├── PacketHeader.ts │ │ │ │ │ ├── Ping.ts │ │ │ │ │ ├── PkAudienceContributionDetail.ts │ │ │ │ │ ├── PkAudienceContributionInfo.ts │ │ │ │ │ ├── PkPlayerInfo.ts │ │ │ │ │ ├── PkPlayerRoundStatistic.ts │ │ │ │ │ ├── PkPlayerStatistic.ts │ │ │ │ │ ├── PkRoundInfo.ts │ │ │ │ │ ├── PushServiceToken.ts │ │ │ │ │ ├── Register.ts │ │ │ │ │ ├── RequestBasicInfo.ts │ │ │ │ │ ├── SdkOption.ts │ │ │ │ │ ├── SettingInfo.ts │ │ │ │ │ ├── SharePlatform.ts │ │ │ │ │ ├── SyncCookie.ts │ │ │ │ │ ├── TokenInfo.ts │ │ │ │ │ ├── Unregister.ts │ │ │ │ │ ├── UpstreamPayload.ts │ │ │ │ │ ├── User.ts │ │ │ │ │ ├── UserInstance.ts │ │ │ │ │ ├── ZtCommonInfo.ts │ │ │ │ │ ├── ZtDrawGiftInfo.ts │ │ │ │ │ ├── ZtLiveActionSignalItem.ts │ │ │ │ │ ├── ZtLiveChatProto.ts │ │ │ │ │ ├── ZtLiveCsCmd.ts │ │ │ │ │ ├── ZtLiveCsEnterRoom.ts │ │ │ │ │ ├── ZtLiveCsHeartbeat.ts │ │ │ │ │ ├── ZtLiveCsUserExit.ts │ │ │ │ │ ├── ZtLiveDownstreamPayloadErrorCode.ts │ │ │ │ │ ├── ZtLiveNotifySignalItem.ts │ │ │ │ │ ├── ZtLivePkProto.ts │ │ │ │ │ ├── ZtLiveScActionSignal.ts │ │ │ │ │ ├── ZtLiveScMessage.ts │ │ │ │ │ ├── ZtLiveScNotifySignal.ts │ │ │ │ │ ├── ZtLiveScStateSignal.ts │ │ │ │ │ ├── ZtLiveScStatusChanged.ts │ │ │ │ │ ├── ZtLiveScTicketInvalid.ts │ │ │ │ │ ├── ZtLiveStateSignalItem.ts │ │ │ │ │ ├── ZtLiveUserIdentity.ts │ │ │ │ │ ├── ZtLiveUserInfo.ts │ │ │ │ │ └── acfun.live.ts │ │ │ │ ├── requests.ts │ │ │ │ └── utils.ts │ │ │ ├── bilibili │ │ │ │ └── index.ts │ │ │ ├── crypto.ts │ │ │ └── ws.ts │ │ └── types.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── knexy │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── index.ts │ └── lib │ │ ├── knexy.ts │ │ ├── migration │ │ ├── init.ts │ │ └── types.ts │ │ └── testing │ │ ├── pg.ts │ │ ├── sqlite.ts │ │ ├── testing.ts │ │ └── types.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── migrations.json ├── nx.json ├── package-lock.json ├── package.json ├── tools ├── generators │ └── .gitkeep └── tsconfig.tools.json └── tsconfig.base.json /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /.vscode 4 | /tmp -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] } 16 | ] 17 | } 18 | ] 19 | } 20 | }, 21 | { 22 | "files": ["*.ts", "*.tsx"], 23 | "extends": ["plugin:@nrwl/nx/typescript"], 24 | "parserOptions": { "project": "./tsconfig.*?.json" }, 25 | "rules": {} 26 | }, 27 | { 28 | "files": ["*.js", "*.jsx"], 29 | "extends": ["plugin:@nrwl/nx/javascript"], 30 | "rules": {} 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://afdian.net/@comen'] 4 | -------------------------------------------------------------------------------- /.github/workflows/node-backend-docker.yaml: -------------------------------------------------------------------------------- 1 | name: "Build docker image for node backend" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - 11 | name: Checkout 12 | uses: actions/checkout@v2 13 | - 14 | name: Set up QEMU 15 | uses: docker/setup-qemu-action@v1 16 | - 17 | name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | - 20 | name: Login to DockerHub 21 | uses: docker/login-action@v1 22 | with: 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | - 26 | name: Build and push 27 | uses: docker/build-push-action@v2 28 | with: 29 | context: . 30 | file: ./apps/node-backend/Dockerfile 31 | platforms: linux/amd64 32 | push: true 33 | tags: 3shain/comen:latest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | .vercel 42 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | /tmp -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "angular.ng-template", 5 | "esbenp.prettier-vscode", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 3Shain 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Comen 2 | 3 | > 此项目已长期没有维护 4 | 5 | [](https://github.com/3Shain/isomorphic-danmaku/LICENSE) 6 | [](http://makeapullrequest.com) 7 | 原项目【bilichat】请查看[bilichat](https://github.com/3Shain/Comen/tree/bilichat)分支 8 | 9 | Comen是一个主要用于在网络直播中向观众展示当前直播间实时评论流的工具。 10 | 11 | ### 主要特性 12 | * 多平台支持:默认提供了Acfun,Bilibili的接入实现。合理的抽象低耦合设计使得新平台的接入也很容易。 13 | * 高可靠性:不只是一个爱好项目。原项目已运行两年的时间,积累了各种对用户体验和异常处理的优化。 14 | * 兼容Youtube评论栏样式,只需修改URL就能迁移。 15 | * 高度自定义 (计划中,TBD) 16 | * 作为原项目的延续,提供用户几乎无感知的兼容。 17 | 18 | 19 | ### 贡献代码 20 | 21 | 本项目主要技术栈为前端Angular+后端Nestjs,即完全使用TypeScript(JavaScript)实现。 22 | 23 | 本项目使用到了nx workspace作为管理代码的工具。本项目是一个monorepo的结构,所以任何本项目开源的部分都能在此仓库找到。 24 | 25 | **代码结构** 26 | ``` 27 | - apps 28 | - core --- 前端项目 29 | - node-backend --- 后端的node(cli)包装 30 | - libs 31 | - backend-core --- 后端的可嵌入式实现 32 | - common --- 常用与扩展代码 33 | - gamma --- 兼容Youtube的评论栏渲染器(Angular组件库) 34 | - isomorphic-danmaku --- 同构弹幕,实现了acfun、bilibili的ws连接 35 | - isomprphic-danmaku-server --- 同构弹幕,服务端only部分 36 | ``` 37 | 38 | **调试运行** 39 | 拉取仓库后,首先安装项目依赖 40 | ```sh 41 | npm install 42 | ``` 43 | 44 | 然后分别打开前端与后端的dev-server 45 | ``` 46 | nx serve core 47 | nx serve node-backend 48 | ``` 49 | 50 | 现在可以对代码进行更改了。任何有关代码更改都会触发自动刷新。 51 | 52 | **我想贡献一些新功能/我想修复一些问题** 53 | 54 | 本项目的主要目标是将一类实时/非实时评论流在任何兼容html5的平台上渲染出来。在抽象设计上,本项目可分成三大部分:评论源(source),过滤器(filter)和渲染器(renderer) 55 | 56 | * 我想添加一个新的平台实现 57 | 58 | 参考 apps/core/src/app/sources 下的文件。你需要实现MessageSource接口,其中connect方法就是返回一个生产Message的可观察对象。 59 | 60 | * 我想修改过滤相关的代码 61 | 62 | 参考 apps/core/src/app/common/filter.ts 。过滤器是通过rxjs的pipe实现的,强烈建议熟练掌握rxjs,或仅在现有的代码上修改。原理上有点类似于后端的middleware,但rxjs的可观察对象概念要复杂一些,涉及到错误处理,subscription和complete的处理。 63 | 另外你还可能需要参考各类Message对象的定义(common包的message.ts)。他们只是单纯的数据对象,不涉及到类也没有成员函数。这些类型定义可能并不稳定。Message由source生成,经过filter后最终流向renderer。但并不是所有类型(以type字段区分的类型)都会被renderer处理。filter也有可能在中间对类型对象数据进行转换/变更。 64 | 65 | 66 | * 我想修改渲染相关的代码 67 | 68 | 所有渲染相关代码都在libs/gamma包内。 69 | -------------------------------------------------------------------------------- /apps/core-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["src/plugins/index.js"], 7 | "rules": { 8 | "@typescript-eslint/no-var-requires": "off", 9 | "no-undef": "off" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/core-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "pluginsFile": "./src/plugins/index", 7 | "supportFile": "./src/support/index.ts", 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/core-e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/core-e2e/screenshots", 11 | "chromeWebSecurity": false 12 | } 13 | -------------------------------------------------------------------------------- /apps/core-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /apps/core-e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po'; 2 | 3 | describe('core', () => { 4 | beforeEach(() => cy.visit('/')); 5 | 6 | it('should display welcome message', () => { 7 | // Custom command example, see `../support/commands.ts` file 8 | cy.login('my-email@something.com', 'myPassword'); 9 | 10 | // Function helper example, see `../support/app.po.ts` file 11 | getGreeting().contains('Welcome to core!'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/core-e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor'); 15 | 16 | module.exports = (on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | 20 | // Preprocess Typescript file using Nx helper 21 | on('file:preprocessor', preprocessTypescript(config)); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/core-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /apps/core-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-namespace 12 | declare namespace Cypress { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | interface Chainable { 15 | login(email: string, password: string): void; 16 | } 17 | } 18 | // 19 | // -- This is a parent command -- 20 | Cypress.Commands.add('login', (email, password) => { 21 | console.log('Custom command example: Login', email, password); 22 | }); 23 | // 24 | // -- This is a child command -- 25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 26 | // 27 | // 28 | // -- This is a dual command -- 29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 30 | // 31 | // 32 | // -- This will overwrite an existing command -- 33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 34 | -------------------------------------------------------------------------------- /apps/core-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /apps/core-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/core-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.e2e.json" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/core/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /apps/core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "parserOptions": { 12 | "project": ["apps/core/tsconfig.*?.json"] 13 | }, 14 | "rules": { 15 | "@angular-eslint/directive-selector": [ 16 | "error", 17 | { 18 | "type": "attribute", 19 | "prefix": "comen", 20 | "style": "camelCase" 21 | } 22 | ], 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "type": "element", 27 | "prefix": "comen", 28 | "style": "kebab-case" 29 | } 30 | ] 31 | } 32 | }, 33 | { 34 | "files": ["*.html"], 35 | "extends": ["plugin:@nrwl/nx/angular-template"], 36 | "rules": {} 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /apps/core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'core', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | astTransformers: { 10 | before: [ 11 | 'jest-preset-angular/build/InlineFilesTransformer', 12 | 'jest-preset-angular/build/StripStylesTransformer', 13 | ], 14 | }, 15 | }, 16 | }, 17 | coverageDirectory: '../../coverage/apps/core', 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 21 | 'jest-preset-angular/build/HTMLCommentSerializer.js', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /apps/core/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://127.0.0.1:4000", 4 | "secure": false, 5 | "headers": { 6 | "Connection": "keep-alive" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /apps/core/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationRef, Component, ElementRef, Inject, ViewChild, ɵdetectChanges } from '@angular/core'; 2 | import { Router, NavigationEnd } from '@angular/router'; 3 | import { nextFrame } from '@comen/common'; 4 | import { ReplaySubject, Subject } from 'rxjs'; 5 | import { CSSINJECT_CONFIG_TOKEN } from './config'; 6 | 7 | export function CONFIG_FACTORY() { 8 | return new ReplaySubject(1); 9 | } 10 | 11 | @Component({ 12 | selector: 'comen-root', 13 | template: ` 14 | `, 15 | providers: [ 16 | { 17 | provide: CSSINJECT_CONFIG_TOKEN, 18 | useFactory: CONFIG_FACTORY 19 | } 20 | ] 21 | }) 22 | export class AppComponent { 23 | 24 | @ViewChild('data') data: ElementRef; 25 | 26 | constructor(private app: ApplicationRef, 27 | private router: Router, 28 | @Inject(CSSINJECT_CONFIG_TOKEN) private config$: Subject) { 29 | /** 30 | * temporary workaround for zoneless router 31 | */ 32 | router.events.subscribe((event) => { 33 | if (event instanceof NavigationEnd) { 34 | // app.tick(); 35 | ɵdetectChanges(this); 36 | } 37 | }); 38 | } 39 | 40 | async ngAfterViewInit() { 41 | // config in css 42 | if ('obsstudio' in window) { 43 | let retryCount = 0; 44 | // wait 10 frame to fetch 45 | while (retryCount < 10) { 46 | await nextFrame(); 47 | const ret = getComputedStyle(this.data.nativeElement, ':after').content; 48 | if (ret != 'none') { 49 | this.config$.next(ret); 50 | return; 51 | } 52 | retryCount++; 53 | } 54 | this.config$.next(null); 55 | } else { 56 | await nextFrame(); 57 | await nextFrame(); 58 | this.config$.next(null); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apps/core/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { APP_INITIALIZER, Injectable, NgModule } from '@angular/core'; 2 | import { AppComponent } from './app.component'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | import { ActivatedRouteSnapshot, CanActivate, Router, RouterModule, UrlTree } from '@angular/router'; 6 | import { ComenSourceModule } from './sources/source.module'; 7 | import { AnalyticsService } from './common/analytics.service'; 8 | 9 | // eslint-disable-next-line 10 | function APPINITIAL(ana: AnalyticsService) { 11 | return () => { return ana.init() }; 12 | } 13 | 14 | @Injectable() 15 | class CompatibleRoutes implements CanActivate { 16 | constructor(private router: Router) { } 17 | canActivate(route: ActivatedRouteSnapshot): UrlTree { 18 | switch (route.url[0].path) { 19 | case 'gkd': 20 | case 'alpha': 21 | return this.router.createUrlTree(['/', 'comment'], { 22 | queryParams: { 23 | ...route.queryParams, 24 | p: 'bilibili', 25 | bilichat: '', 26 | id: +route.params.id 27 | } 28 | }); 29 | case 'bilibili': 30 | return this.router.createUrlTree(['/', 'comment'], { 31 | queryParams: { 32 | ...route.queryParams, 33 | p: 'bilibili', 34 | id: +route.params.id 35 | } 36 | }); 37 | case 'acfun': 38 | return this.router.createUrlTree(['/', 'comment'], { 39 | queryParams: { 40 | ...route.queryParams, 41 | p: 'acfun', 42 | id: +route.params.id 43 | } 44 | }); 45 | } 46 | throw new Error('NOT EXPECTED ROUTE'); 47 | } 48 | 49 | } 50 | 51 | @NgModule({ 52 | declarations: [ 53 | AppComponent 54 | ], 55 | imports: [ 56 | BrowserAnimationsModule, 57 | ComenSourceModule, 58 | HttpClientModule, 59 | RouterModule.forRoot([ 60 | { 61 | path: '', 62 | pathMatch: 'full', 63 | loadChildren: () => import('./pages/home/home.module').then(m => m.HomeModule) 64 | }, 65 | { 66 | path: 'comment', 67 | loadChildren: () => import('./pages/comment/comment.module').then(m => m.CommentModule) 68 | }, 69 | /** (bilichat) compatible routes */ 70 | { 71 | path: 'gkd/:id', 72 | canActivate: [CompatibleRoutes], 73 | children: [] 74 | }, { 75 | path: 'alpha/:id', 76 | canActivate: [CompatibleRoutes], 77 | children: [] 78 | }, { 79 | path: 'bilibili/:id', 80 | canActivate: [CompatibleRoutes], 81 | children: [] 82 | }, { 83 | path: 'acfun/:id', 84 | canActivate: [CompatibleRoutes], 85 | children: [] 86 | } 87 | ]) 88 | ], 89 | providers: [AnalyticsService, 90 | { 91 | provide: APP_INITIALIZER, 92 | useFactory: APPINITIAL, 93 | multi: true, 94 | deps: [AnalyticsService] 95 | }, CompatibleRoutes], 96 | bootstrap: [AppComponent] 97 | }) 98 | export class AppModule { } 99 | -------------------------------------------------------------------------------- /apps/core/src/app/common/analytics.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import posthog from 'posthog-js'; 3 | import { environment } from '../../environments/environment'; 4 | 5 | @Injectable() 6 | export class AnalyticsService { 7 | 8 | on = false; 9 | disabled = false; 10 | 11 | init() { 12 | if (!environment.production) { 13 | return Promise.resolve(); 14 | } 15 | return new Promise((res) => { 16 | posthog.init(environment.analytics_token, { 17 | loaded: () => { 18 | res(void 0); 19 | if(!this.disabled){ 20 | this.on = true; 21 | } 22 | }, 23 | api_host: 'https://analytics.3shain.com', 24 | capture_pageview: false, 25 | disable_cookie: false, // it's meaningless 26 | autocapture: false, 27 | disable_session_recording: true 28 | } as unknown); 29 | setTimeout(res,1000); // in case analytic server is down 30 | }); 31 | } 32 | 33 | event(event: string, data: any) { 34 | if (environment.production&&this.on) { 35 | posthog.capture(event, data); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /apps/core/src/app/common/emoji.spec.ts: -------------------------------------------------------------------------------- 1 | import { emojiExpressionLexer } from './emoji'; 2 | 3 | const TEXT_MAP = { 4 | 狗头: 'goutou', 5 | 猫头: 'maotou' 6 | } 7 | 8 | describe('emoji', () => { 9 | 10 | 11 | it.each([ 12 | ['总之测试一下(狗头', [ 13 | { 14 | type: 'text', 15 | content: '总之测试一下' 16 | }, 17 | { 18 | type: 'emoji', 19 | url: 'goutou' 20 | } 21 | ]], 22 | ['总之测试一下(猫头', [ 23 | { 24 | type: 'text', 25 | content: '总之测试一下' 26 | }, 27 | { 28 | type: 'emoji', 29 | url: 'maotou' 30 | } 31 | ]], 32 | ['总之测试一下(狗头)', [ 33 | { 34 | type: 'text', 35 | content: '总之测试一下' 36 | }, 37 | { 38 | type: 'emoji', 39 | url: 'goutou' 40 | } 41 | ]], 42 | ['总之测试一下(狗头)233', [ 43 | { 44 | type: 'text', 45 | content: '总之测试一下' 46 | }, 47 | { 48 | type: 'emoji', 49 | url: 'goutou' 50 | }, 51 | { 52 | type: 'text', 53 | content: '233' 54 | } 55 | ]], 56 | ['总之测试一下(狗头 233', [ 57 | { 58 | type: 'text', 59 | content: '总之测试一下' 60 | }, 61 | { 62 | type: 'emoji', 63 | url: 'goutou' 64 | }, 65 | { 66 | type: 'text', 67 | content: ' 233' 68 | } 69 | ]], 70 | ['总之测试一下(狗头 233', [ 71 | { 72 | type: 'text', 73 | content: '总之测试一下' 74 | }, 75 | { 76 | type: 'emoji', 77 | url: 'goutou' 78 | }, 79 | { 80 | type: 'text', 81 | content: ' 233' 82 | }] 83 | ], 84 | ['(狗头)(((= =)(((((', [ 85 | { 86 | type: 'emoji', 87 | url: 'goutou' 88 | }, 89 | { 90 | type: 'text', 91 | content: '(((= =)(((((' 92 | } 93 | ]] 94 | ])('should parse %s', (a, b) => { 95 | 96 | const ret = emojiExpressionLexer((s) => TEXT_MAP[s], a); 97 | expect(ret).toEqual(b) 98 | }) 99 | }) -------------------------------------------------------------------------------- /apps/core/src/app/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './filter'; -------------------------------------------------------------------------------- /apps/core/src/app/config/config.spec.ts: -------------------------------------------------------------------------------- 1 | import { serializeConfiguration, parseConfiguration } from './config'; 2 | 3 | describe('configuration', () => { 4 | 5 | beforeAll(() => { 6 | // 测试体验极差🤮 7 | if (typeof TextEncoder === 'undefined') { 8 | (global as any).TextEncoder = require('util').TextEncoder; 9 | (global as any).TextDecoder = require('util').TextDecoder; 10 | } 11 | }) 12 | 13 | it('should be serialized and deserialized correctly', () => { 14 | const conf = { 15 | var1: 0, 16 | var2: 'test', 17 | var3: false, 18 | var4: { 19 | var5: 1, 20 | var6: { 21 | var7: 'test2', 22 | '!!!': '???' 23 | } 24 | } 25 | }; 26 | 27 | const serialized = serializeConfiguration(conf); 28 | const ret = parseConfiguration(serialized, null); 29 | 30 | expect(ret).toEqual(conf); 31 | }); 32 | 33 | it('should work with blob attachments',()=>{ 34 | const conf = { 35 | blob: new Uint8Array(16).fill(0x99), 36 | nested: { 37 | blob2: new Uint8Array(32).fill(0x86) 38 | } 39 | }; 40 | 41 | const serialized = serializeConfiguration(conf); 42 | console.log(serialized); 43 | const ret = parseConfiguration(serialized,{}); 44 | 45 | expect(ret).toEqual(conf); 46 | }) 47 | }) -------------------------------------------------------------------------------- /apps/core/src/app/config/index.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | export { ComenConfiguration, parseConfiguration, serializeConfiguration,DEFAULT_CONFIG } from './config'; 5 | export { mergeQueryParameters } from './merge-query'; 6 | 7 | export const CSSINJECT_CONFIG_TOKEN = new InjectionToken>('config injection'); -------------------------------------------------------------------------------- /apps/core/src/app/config/merge-query.ts: -------------------------------------------------------------------------------- 1 | import { ComenConfiguration } from './config'; 2 | 3 | export function mergeQueryParameters(query:any,config:ComenConfiguration){ 4 | if(query.bilichat!=undefined){ 5 | /** 6 | * these query parameters is for bilichat compatibility, do not use if you don't know about it 7 | */ 8 | (config as any).bilichat = true; 9 | // in bilichat, groupSimilar is default on 10 | config.groupSimilar = query.groupSimilar ?? true; 11 | config.groupSimilarWindow = ('groupSimilarWindow' in query)? parseInt(query.groupSimilarWindow) : 5; 12 | config.hideTimestamp = true; // BILICHAT doesn't provide timestamp 13 | 14 | // TODO: implement all bilichat compatiable options 15 | if(query.loadAvatar=='false'){ 16 | config.disableAvatarPreload = true; 17 | } 18 | if(query.levelFilter!=undefined){ 19 | config.levelFilter = parseInt(query.levelFilter); 20 | } 21 | if(query.hideGiftDanmaku=='false'){ 22 | config.showGiftAutoDammaku = true; 23 | } 24 | if(query.wordFilter!=undefined){ 25 | const words = (query.wordFilter as string).split(','); 26 | config.wordBlacklist?.push(...words); 27 | } 28 | if(query.giftOnly=='true'){ 29 | config.typeFilterControlBit = 0b1001; // is that correct? 30 | } 31 | if(query.showGift=='false'){ 32 | config.typeFilterControlBit = config.typeFilterControlBit ^ 0b0010; 33 | } 34 | if(query.showJapanese=='true'){ 35 | config.useJapaneseSC = true; 36 | } 37 | if(query.blackList!=undefined){ 38 | const blacklist = (query.blackList as string).split(',').map((v)=>parseInt(v)); 39 | config.userBlacklist?.push(...blacklist); 40 | } 41 | if(query.pure=='true'){ 42 | config.disableAvatarPreload = true; 43 | // No other effect 44 | } 45 | if(query.minGiftValue!=undefined){ 46 | config.minGiftValue = parseFloat(query.minGiftValue); 47 | } 48 | if(query.silverGoldRatio!=undefined){ 49 | config.silverGoldRatio = parseInt(query.silverGoldRatio); 50 | } 51 | } else { 52 | 53 | } 54 | if(query.disableAnalytics == 'true'){ 55 | config.disableAnalytics = true; 56 | } 57 | config.platform = query.p; 58 | config.roomId = query.id; 59 | return config; 60 | } 61 | -------------------------------------------------------------------------------- /apps/core/src/app/pages/comment/comment.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { GammaModule } from '@comen/gamma'; 5 | import { CommentPage } from './comment.page'; 6 | 7 | 8 | @NgModule({ 9 | declarations: [CommentPage], 10 | imports: [ 11 | CommonModule, 12 | GammaModule, 13 | RouterModule.forChild([{ 14 | path: '', //compatiability 15 | component: CommentPage 16 | }]) 17 | ] 18 | }) 19 | export class CommentModule { 20 | } -------------------------------------------------------------------------------- /apps/core/src/app/pages/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { HomePage } from './home.page'; 5 | import { ReactiveComponentModule } from '@ngrx/component'; 6 | import { ReactiveFormsModule } from '@angular/forms'; 7 | 8 | @NgModule({ 9 | declarations: [HomePage], 10 | imports: [ 11 | CommonModule, 12 | ReactiveComponentModule, 13 | ReactiveFormsModule, 14 | RouterModule.forChild([{ 15 | path: '', 16 | pathMatch: 'full', 17 | component: HomePage 18 | }]) 19 | ] 20 | }) 21 | export class HomeModule { 22 | 23 | } -------------------------------------------------------------------------------- /apps/core/src/app/pages/home/home.page.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | box-sizing: border-box; 3 | padding: 16px; 4 | display: block; 5 | width: 100vw; 6 | height: 100vh; 7 | background-color: #fafbfc; 8 | } 9 | 10 | .in { 11 | color: #586069; 12 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace!important;; 13 | } 14 | 15 | .icon-button { 16 | border-radius: 8px; 17 | cursor: pointer; 18 | padding: 16px; 19 | margin: 16px; 20 | &>svg{ 21 | height: 72px; 22 | width: 72px; 23 | } 24 | 25 | color: #aabaca; 26 | 27 | transition: color 0.5s ease, background-color 0.5s ease; 28 | 29 | &.selected { 30 | color: white; 31 | background-color: #aabaca; 32 | } 33 | 34 | &:hover{ 35 | color: var(--primary-color); 36 | background-color: #eaebec; 37 | } 38 | 39 | &.bilibili { 40 | --primary-color: #00a1d6; 41 | } 42 | 43 | &.acfun { 44 | --primary-color: #FD4C5D; 45 | } 46 | } 47 | 48 | #cover { 49 | height: calc(100% - 48px); 50 | display: flex; 51 | flex-direction: column; 52 | align-items: center; 53 | justify-content: center; 54 | 55 | &>#platforms { 56 | display: flex; 57 | } 58 | 59 | .generate-link { 60 | cursor: pointer; 61 | border-radius: 5px; 62 | padding: 10px 16px; 63 | color: #555; 64 | border: 1px #cccccc solid; 65 | font-variant-ligatures: none; 66 | font-family: "Fira Mono", "Andale Mono", "Consolas", monospace; 67 | letter-spacing: 0px; 68 | display: inline-block; 69 | width: 400px; 70 | line-height: 24px; 71 | transition: background-color 0.5s ease; 72 | user-select: none; 73 | 74 | 75 | &:hover { 76 | background-color: #eaebec; 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /apps/core/src/app/pages/home/home.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | import { Title } from '@angular/platform-browser'; 4 | import { BehaviorSubject, combineLatest, Subject, Subscription } from 'rxjs'; 5 | import { map, startWith } from 'rxjs/operators'; 6 | 7 | @Component({ 8 | selector: 'comen-home', 9 | templateUrl: './home.page.html', 10 | styleUrls: [ 11 | './home.page.scss' 12 | ] 13 | }) 14 | // eslint-disable-next-line 15 | export class HomePage implements OnInit, OnDestroy { 16 | 17 | platform$: Subject = new BehaviorSubject(localStorage.getItem('platform') ?? 'bilibili'); 18 | 19 | roomId = new FormControl(localStorage.getItem('roomId') ?? '123456'); 20 | roomIdSubscription: Subscription; 21 | 22 | generatedLink = combineLatest([this.platform$, this.roomId.valueChanges.pipe(startWith(this.roomId.value))]).pipe( 23 | map(([platform, id]) => { 24 | return `${window.location.origin}/${platform}/${id}` 25 | }) 26 | ) 27 | 28 | constructor(private title: Title) { 29 | title.setTitle('主页'); 30 | if (window.location.host == 'bilichat.3shain.com') { 31 | window.location.href = 'https://github.com/3Shain/Comen/tree/bilichat'; 32 | } 33 | } 34 | 35 | ngOnInit() { 36 | this.roomIdSubscription = this.roomId.valueChanges.subscribe(id => { 37 | localStorage.setItem('roomId', id) 38 | }); 39 | } 40 | 41 | setPlatform(platform: string) { 42 | this.platform$.next(platform); 43 | localStorage.setItem('platform', platform); 44 | } 45 | 46 | clickLink(event:Event){ 47 | (event.target as HTMLInputElement).select(); 48 | document.execCommand('copy'); 49 | } 50 | 51 | ngOnDestroy() { 52 | this.roomIdSubscription.unsubscribe(); 53 | } 54 | } -------------------------------------------------------------------------------- /apps/core/src/app/sources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './source'; 2 | export * from './acfun'; 3 | export * from './bilibili'; 4 | export * from './source.module'; -------------------------------------------------------------------------------- /apps/core/src/app/sources/source.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { AcfunSource } from './acfun'; 3 | import { BilibiliSource } from './bilibili'; 4 | import { SOURCE_PROVIDER } from './source'; 5 | 6 | //feature module 7 | @NgModule({ 8 | providers: [ 9 | { 10 | provide: SOURCE_PROVIDER, 11 | multi: true, 12 | useClass: BilibiliSource 13 | }, { 14 | provide: SOURCE_PROVIDER, 15 | multi: true, 16 | useClass: AcfunSource 17 | } 18 | ] 19 | }) 20 | export class ComenSourceModule { } -------------------------------------------------------------------------------- /apps/core/src/app/sources/source.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Message } from '@comen/common'; 4 | 5 | export interface MessageSource { 6 | readonly type: string; 7 | connect(options: unknown): Observable; 8 | } 9 | 10 | export const SOURCE_PROVIDER = new InjectionToken('Message sources'); -------------------------------------------------------------------------------- /apps/core/src/assets/bilichat_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3Shain/Comen/f420d76d6a76659e7c69ac0a0e1802cdc1440b56/apps/core/src/assets/bilichat_icon.png -------------------------------------------------------------------------------- /apps/core/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | analytics_host: 'https://analytics.3shain.com', 4 | analytics_token: 'whrt8_XCHW635Gq2UKtlOIVLOdjuhZVyy3UE-z-SYZc' 5 | }; 6 | -------------------------------------------------------------------------------- /apps/core/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | analytics_host: null, 8 | analytics_token: null 9 | }; 10 | 11 | /* 12 | * For easier debugging in development mode, you can import the following file 13 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 14 | * 15 | * This import should be commented out in production mode because it will have a negative impact 16 | * on performance if an error is thrown. 17 | */ 18 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 19 | -------------------------------------------------------------------------------- /apps/core/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3Shain/Comen/f420d76d6a76659e7c69ac0a0e1802cdc1440b56/apps/core/src/favicon.ico -------------------------------------------------------------------------------- /apps/core/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Comen by 3Shain 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/core/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { AppModule } from './app/app.module'; 4 | import { environment } from './environments/environment'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | platformBrowserDynamic() 11 | .bootstrapModule(AppModule) 12 | .catch((err) => console.error(err)); 13 | -------------------------------------------------------------------------------- /apps/core/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /apps/core/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | html,body { 3 | margin: 0; 4 | } -------------------------------------------------------------------------------- /apps/core/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /apps/core/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | }, 6 | "files": ["src/main.ts", "src/polyfills.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /apps/core/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.editor.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /apps/core/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/node-backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } 2 | -------------------------------------------------------------------------------- /apps/node-backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # make sure context is the root of the whole repo! 2 | FROM node:12-buster AS build 3 | WORKDIR /app 4 | COPY ./package-lock.json /app 5 | COPY ./package.json /app 6 | COPY ./decorate-angular-cli.js /app 7 | RUN ["npm","install","--unsafe-perm"] 8 | COPY . /app 9 | RUN ["npx","nx","build","core","--prod"] 10 | RUN ["npx","nx","build","node-backend","--prod"] 11 | 12 | FROM node:12-alpine AS final 13 | WORKDIR /app 14 | # backend packages only 15 | COPY --from=build /app/dist/apps/node-backend /app 16 | COPY --from=build /app/package-lock.json /app 17 | RUN ["npm","install","--production"] 18 | ENV PORT=4000 19 | EXPOSE 4000 20 | CMD ["node","main.js"] -------------------------------------------------------------------------------- /apps/node-backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'node-backend', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsConfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | transform: { 10 | '^.+\\.[tj]s$': 'ts-jest', 11 | }, 12 | moduleFileExtensions: ['ts', 'js', 'html'], 13 | coverageDirectory: '../../coverage/apps/node-backend', 14 | }; 15 | -------------------------------------------------------------------------------- /apps/node-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "comen", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "main.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/3Shain/Comen" 9 | }, 10 | "author": { 11 | "name": "3Shain" 12 | }, 13 | "description": "Comment box rendered in html5 for live stream", 14 | "keywords": ["bilibili","acfun","youtube","livechat","comen","comment"], 15 | "dependencies": { 16 | "@nestjs/common": "^7.0.0", 17 | "@nestjs/core": "^7.0.0", 18 | "@nestjs/platform-express": "^7.0.0", 19 | "@nestjs/serve-static": "^2.1.4", 20 | "cache-manager": "^3.4.0", 21 | "cache-manager-fs": "^1.0.8", 22 | "cache-manager-redis-store": "^2.0.0", 23 | "reflect-metadata": "^0.1.13", 24 | "rxjs": "~6.5.5", 25 | "tslib": "^2.0.0" 26 | } 27 | } -------------------------------------------------------------------------------- /apps/node-backend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3Shain/Comen/f420d76d6a76659e7c69ac0a0e1802cdc1440b56/apps/node-backend/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/node-backend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/node-backend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/node-backend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapBackendCore } from '@comen/backend-core'; 2 | import { environment } from './environments/environment'; 3 | import { join, resolve } from 'path'; 4 | 5 | bootstrapBackendCore({ 6 | dev: !environment.production, 7 | frontendPath: join(resolve(__dirname), 'frontend') 8 | }).catch(console.error); -------------------------------------------------------------------------------- /apps/node-backend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"], 6 | "emitDecoratorMetadata": true, 7 | "target": "es2015" 8 | }, 9 | "exclude": ["**/*.spec.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/node-backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/node-backend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /decorate-angular-cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching 3 | * and faster execution of tasks. 4 | * 5 | * It does this by: 6 | * 7 | * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command. 8 | * - Symlinking the ng to nx command, so all commands run through the Nx CLI 9 | * - Updating the package.json postinstall script to give you control over this script 10 | * 11 | * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it. 12 | * Every command you run should work the same when using the Nx CLI, except faster. 13 | * 14 | * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case, 15 | * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked. 16 | * The Nx CLI simply does some optimizations before invoking the Angular CLI. 17 | * 18 | * To opt out of this patch: 19 | * - Replace occurrences of nx with ng in your package.json 20 | * - Remove the script from your postinstall script in your package.json 21 | * - Delete and reinstall your node_modules 22 | */ 23 | 24 | const fs = require('fs'); 25 | const os = require('os'); 26 | const cp = require('child_process'); 27 | const isWindows = os.platform() === 'win32'; 28 | let output; 29 | try { 30 | output = require('@nrwl/workspace').output; 31 | } catch (e) { 32 | console.warn('Angular CLI could not be decorated to enable computation caching. Please ensure @nrwl/workspace is installed.'); 33 | process.exit(0); 34 | } 35 | 36 | /** 37 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still 38 | * invoke the Nx CLI and get the benefits of computation caching. 39 | */ 40 | function symlinkNgCLItoNxCLI() { 41 | try { 42 | const ngPath = './node_modules/.bin/ng'; 43 | const nxPath = './node_modules/.bin/nx'; 44 | if (isWindows) { 45 | /** 46 | * This is the most reliable way to create symlink-like behavior on Windows. 47 | * Such that it works in all shells and works with npx. 48 | */ 49 | ['', '.cmd', '.ps1'].forEach(ext => { 50 | if (fs.existsSync(nxPath + ext)) fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext)); 51 | }); 52 | } else { 53 | // If unix-based, symlink 54 | cp.execSync(`ln -sf ./nx ${ngPath}`); 55 | } 56 | } 57 | catch(e) { 58 | output.error({ title: 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + e.message }); 59 | throw e; 60 | } 61 | } 62 | 63 | try { 64 | symlinkNgCLItoNxCLI(); 65 | require('@nrwl/cli/lib/decorate-cli').decorateCli(); 66 | output.log({ title: 'Angular CLI has been decorated to enable computation caching.' }); 67 | } catch(e) { 68 | output.error({ title: 'Decoration of the Angular CLI did not complete successfully' }); 69 | } 70 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ 3 | '/apps/core', 4 | '/libs/gamma', 5 | '/apps/api', 6 | '/libs/backend-core', 7 | '/libs/testing', 8 | '/libs/knexy', 9 | '/libs/isomorphic-danmaku', 10 | '/libs/isomorphic-danmaku-server', 11 | '/libs/common', 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset'); 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/backend-core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } 2 | -------------------------------------------------------------------------------- /libs/backend-core/README.md: -------------------------------------------------------------------------------- 1 | # backend-core 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test backend-core` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/backend-core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'backend-core', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsConfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 14 | coverageDirectory: '../../coverage/libs/backend-core', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/backend-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/entry'; -------------------------------------------------------------------------------- /libs/backend-core/src/lib/backend-core.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BilibiliController } from './controllers/bilibili.controller'; 3 | 4 | import { AcfunController } from './controllers/acfun.controller'; 5 | import { BilibiliUserService } from './services/bili-user.service'; 6 | 7 | @Module({ 8 | controllers: [AcfunController, BilibiliController], 9 | providers: [ 10 | BilibiliUserService 11 | ], 12 | }) 13 | export class BackendCoreModule { } 14 | -------------------------------------------------------------------------------- /libs/backend-core/src/lib/controllers/acfun.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, ParseIntPipe, Query, Res } from "@nestjs/common"; 2 | import { Response } from "express"; 3 | import { getAcfunRoomInfo } from 'isomorphic-danmaku-server'; 4 | 5 | @Controller('acfun') 6 | export class AcfunController { 7 | 8 | @Get('getRoomInfo') 9 | async getRoomInfo(@Query('roomid', ParseIntPipe) roomid: number, 10 | @Res() res: Response) { 11 | return res.json(await getAcfunRoomInfo(roomid)); 12 | } 13 | } -------------------------------------------------------------------------------- /libs/backend-core/src/lib/controllers/bilibili.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query, ParseIntPipe, Res } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | import { BilibiliUserService } from '../services/bili-user.service'; 4 | import { getBilibiliRoomInfo } from 'isomorphic-danmaku-server'; 5 | 6 | @Controller('bili') 7 | export class BilibiliController { 8 | constructor(private bilibiliUser: BilibiliUserService) { 9 | 10 | } 11 | 12 | @Get('getRoomInfo') 13 | async getRoomInfo(@Query('roomid', ParseIntPipe) roomid: number) { 14 | return await getBilibiliRoomInfo(roomid, { 15 | fetchGift: true, 16 | fetchHistoryDanmaku: false 17 | }); 18 | } 19 | 20 | @Get('getAvatar') 21 | async getAvatar(@Query('uid', ParseIntPipe) uid: number, @Res() resp: Response) { 22 | // cache 23 | const cached = await this.bilibiliUser.getUserInfoFromCache(uid); 24 | if (!cached.temp) { 25 | resp.setHeader("Cache-Control", `public,max-age=${3 * 24 * 60 * 60}`); 26 | } 27 | return resp.json({ 28 | url: cached.face 29 | }); 30 | } 31 | } -------------------------------------------------------------------------------- /libs/backend-core/src/lib/entry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is not a production server yet! 3 | * This is only a minimal backend to get started. 4 | */ 5 | 6 | import { CacheModule, Logger } from '@nestjs/common'; 7 | import { NestFactory } from '@nestjs/core'; 8 | import { BackendCoreModule } from './backend-core.module'; 9 | import 'reflect-metadata'; import * as redisStore from 'cache-manager-redis'; 10 | import * as fsStore from 'cache-manager-fs'; 11 | import { ServeStaticModule } from '@nestjs/serve-static'; 12 | import { tmpdir } from 'os'; 13 | import { join } from 'path'; 14 | 15 | export interface ComenBackendOptions { 16 | dev: boolean; 17 | frontendPath: string; 18 | } 19 | 20 | export async function bootstrapBackendCore(options: ComenBackendOptions) { 21 | initModuleDependencies(options); 22 | const app = await NestFactory.create(BackendCoreModule); 23 | const globalPrefix = 'api'; 24 | app.setGlobalPrefix(globalPrefix); 25 | const port = process.env.PORT || 4000; 26 | await app.listen(port, () => { 27 | Logger.log('Listening at http://localhost:' + port); 28 | }); 29 | } 30 | 31 | function initModuleDependencies(options: ComenBackendOptions) { 32 | const modules = [CacheModule.registerAsync({ 33 | useFactory: () => { 34 | if (process.env.REDIS_HOST) { 35 | return { 36 | store: redisStore, 37 | host: process.env.REDIS_HOST, 38 | port: parseInt(process.env.REDIS_PORT ?? "6379"), 39 | auth_pass: process.env.REDIS_AUTH, 40 | db: parseInt(process.env.REDIS_DB ?? "0") 41 | } 42 | } 43 | return { 44 | store: fsStore, 45 | maxsize: 1000 * 1000 * 1000, // 1Gib, 46 | path: tmpdir() + '/comen', 47 | preventfill: true 48 | }; 49 | } 50 | })]; 51 | if (!options.dev) { 52 | modules.push( 53 | ServeStaticModule.forRoot({ 54 | rootPath: options.frontendPath 55 | }), 56 | ServeStaticModule.forRoot({ 57 | rootPath: join(options.frontendPath, 'assets/css4obs'), 58 | serveRoot: '/css4obs' 59 | }) 60 | ); 61 | } 62 | Reflect.defineMetadata("imports", modules, BackendCoreModule); 63 | } -------------------------------------------------------------------------------- /libs/backend-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/backend-core/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"], 8 | "target": "es6" 9 | }, 10 | "exclude": ["**/*.spec.ts"], 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /libs/backend-core/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /libs/common/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } 2 | -------------------------------------------------------------------------------- /libs/common/README.md: -------------------------------------------------------------------------------- 1 | # common 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test common` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/common/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'common', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsConfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | transform: { 10 | '^.+\\.[tj]sx?$': 'ts-jest', 11 | }, 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 13 | coverageDirectory: '../../coverage/libs/common', 14 | }; 15 | -------------------------------------------------------------------------------- /libs/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@comen/common", 3 | "version": "0.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /libs/common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/message'; 2 | export * from './lib/rx'; 3 | export * from './lib/utils'; -------------------------------------------------------------------------------- /libs/common/src/lib/filter.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "./message"; 2 | 3 | export type FilterNextFunction = (msg:Message)=>unknown; 4 | export type FilterFunction = (next:FilterNextFunction)=>FilterNextFunction; -------------------------------------------------------------------------------- /libs/common/src/lib/message.ts: -------------------------------------------------------------------------------- 1 | 2 | type BadgeInfo = { 3 | type: string; 4 | aria_label?: string; 5 | badge: string | Blob; 6 | } 7 | 8 | type RichTextNode = { 9 | type: 'text'; 10 | content: string; 11 | } | { 12 | type: 'image'; 13 | url: string; 14 | width?: number; 15 | height?: number; 16 | style?: string; 17 | } | { 18 | type: 'emoji'; 19 | url: string; 20 | } 21 | 22 | type RichText = { 23 | nodes: RichTextNode[]; 24 | } 25 | 26 | type PlatformBase = { 27 | platformUserId: number; 28 | platformUserLevel: number; 29 | platformUserExtra: unknown; 30 | } 31 | 32 | type BaseTextMessage = { 33 | avatar: string | Blob; 34 | username: string; 35 | 36 | // extensions 37 | badges: BadgeInfo[]; 38 | usertype: number; // 0 normal 1 member 2 mod 3 both (bit) 39 | platformUserId: number; 40 | platformUserLevel: number; 41 | platformUserExtra: unknown; 42 | } 43 | 44 | type TextMessage = { 45 | type: 'text'; 46 | content: string; 47 | } & BaseTextMessage; 48 | 49 | type RichTextMessage = { 50 | type: 'richtext'; 51 | richtext: RichText; 52 | } & BaseTextMessage; 53 | 54 | type PaidMessage = { 55 | type: 'paid'; 56 | content: string; 57 | avatar: string | Blob; 58 | username: string; 59 | itemInfo: string; // e.g.: $500 60 | price: number; 61 | 62 | // extension 63 | platformUserId: number; 64 | } 65 | 66 | type MemberMessage = { 67 | type: 'member'; 68 | avatar: string | Blob; 69 | username: string; 70 | itemInfo: string; // e.g.: Welcome to ... 71 | 72 | price: number; 73 | 74 | //extension 75 | platformMemberType: number; 76 | platformUserId: number; 77 | platformPrice: number; 78 | } 79 | 80 | type StickerMessage = { 81 | type: 'sticker'; 82 | avatar: string | Blob; 83 | username: string; 84 | sticker: string | Blob; 85 | itemInfo: string; // name: 86 | price: number; 87 | amount: number; 88 | 89 | //extension 90 | platformUserId: number; 91 | // 非真实价值 92 | platformPrice: number; 93 | } 94 | 95 | // Special 96 | type FoldMessage = { 97 | type: 'fold'; 98 | targetMessage: TextMessage; 99 | } 100 | 101 | type BlankMessage = { 102 | type: 'blank'; 103 | } 104 | 105 | type SystemMessage = { 106 | type: 'system'; 107 | // eslint-disable-next-line 108 | data: any; 109 | } 110 | 111 | type LiveStartMessage = { 112 | type: 'livestart'; 113 | } 114 | 115 | type LiveStopMessage = { 116 | type: 'livestop'; 117 | } 118 | 119 | 120 | type Message = TextMessage | PaidMessage | MemberMessage | StickerMessage 121 | | FoldMessage | RichTextMessage | SystemMessage | LiveStartMessage | LiveStopMessage | BlankMessage; 122 | 123 | export { 124 | TextMessage, PaidMessage, MemberMessage, StickerMessage, 125 | Message, RichTextMessage, BlankMessage, SystemMessage, LiveStartMessage, LiveStopMessage 126 | }; 127 | 128 | export { RichText, RichTextNode }; 129 | -------------------------------------------------------------------------------- /libs/common/src/lib/rx.ts: -------------------------------------------------------------------------------- 1 | import { Observable, OperatorFunction } from 'rxjs'; 2 | import { Injectable, NgZone } from '@angular/core'; 3 | 4 | /** 5 | * For one trial observable 6 | * @param abort 7 | */ 8 | export function abortable(abort: AbortController): OperatorFunction { 9 | return (upstream) => { 10 | return new Observable((observer) => { 11 | const handler = () => { 12 | unsub.unsubscribe(); 13 | observer.error('ABORTED'); 14 | abort.signal.removeEventListener('abort', handler); 15 | } 16 | const unsub = upstream.subscribe({ 17 | next: (v) => { 18 | observer.next(v); 19 | }, 20 | error: (e) => { 21 | observer.error(e); 22 | }, 23 | complete: () => { 24 | observer.complete(); 25 | abort.signal.removeEventListener('abort', handler); 26 | } 27 | }); 28 | abort.signal.addEventListener('abort', handler); 29 | }); 30 | } 31 | } 32 | 33 | @Injectable() 34 | export class RxZone { 35 | 36 | constructor(private zone: NgZone) { } 37 | 38 | subscribeOutsideAngular(): OperatorFunction { 39 | return (source) => { 40 | return new Observable((observer) => { 41 | return this.zone.runOutsideAngular(() => source.subscribe(observer)); 42 | }); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /libs/common/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { Observable, OperatorFunction } from 'rxjs'; 2 | 3 | export function waitTimeout(time:number){ 4 | return new Promise((res)=>{ 5 | setTimeout(res,time); 6 | }) 7 | } 8 | 9 | export function waitUntilVisible(): OperatorFunction { 10 | return (upstream) => { 11 | return new Observable(observer => { 12 | return upstream.subscribe({ 13 | next(x) { 14 | if (document.visibilityState == 'visible') { 15 | observer.next(x); 16 | return; 17 | } 18 | const teardown = registerVisibilityChange(v => { 19 | v && observer.next(x), teardown(); 20 | }); 21 | }, 22 | error: observer.error.bind(observer), 23 | complete: observer.complete.bind(observer) 24 | }) 25 | }) 26 | } 27 | } 28 | 29 | export function registerVisibilityChange(cb: (visible: boolean) => unknown) { 30 | // eslint-disable-next-line 31 | if ((window as any).obsstudio) { 32 | const handler = (ev: CustomEvent) => { 33 | cb(ev.detail.visible); 34 | } 35 | window.addEventListener('obsSourceVisibleChanged', handler); 36 | return () => { 37 | document.removeEventListener('obsSourceVisibleChanged', handler); 38 | } 39 | } else { 40 | const handler = () => { 41 | cb(document.visibilityState === 'visible'); 42 | }; 43 | document.addEventListener('visibilitychange', handler); 44 | return () => { 45 | document.removeEventListener('visibilitychange', handler); 46 | } 47 | } 48 | } 49 | 50 | export function nextFrame() { 51 | return new Promise((res) => { 52 | requestAnimationFrame(res); 53 | }); 54 | } 55 | 56 | export function easeInOutSine(x: number): number { 57 | return -(Math.cos(Math.PI * x) - 1) / 2; 58 | } -------------------------------------------------------------------------------- /libs/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/common/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["**/*.spec.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /libs/common/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /libs/gamma/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "parserOptions": { 12 | "project": ["libs/gamma/tsconfig.*?.json"] 13 | }, 14 | "rules": { 15 | "@angular-eslint/directive-selector": [ 16 | "error", 17 | { 18 | "type": "attribute", 19 | "prefix": "comen", 20 | "style": "camelCase" 21 | } 22 | ], 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "type": "element", 27 | "prefix": "comen", 28 | "style": "kebab-case" 29 | } 30 | ] 31 | } 32 | }, 33 | { 34 | "files": ["*.html"], 35 | "extends": ["plugin:@nrwl/nx/angular-template"], 36 | "rules": {} 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /libs/gamma/README.md: -------------------------------------------------------------------------------- 1 | # Gamma 2 | 3 | Gamma is a youtube-live-ish comment renderer of comen project. This is the third generation of renderer (from previous project "bilichat") so called "gamma". -------------------------------------------------------------------------------- /libs/gamma/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'gamma', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | astTransformers: { 10 | before: [ 11 | 'jest-preset-angular/build/InlineFilesTransformer', 12 | 'jest-preset-angular/build/StripStylesTransformer', 13 | ], 14 | }, 15 | }, 16 | }, 17 | coverageDirectory: '../../coverage/libs/gamma', 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 21 | 'jest-preset-angular/build/HTMLCommentSerializer.js', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /libs/gamma/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/gamma.module'; 2 | export * from './lib/message-provider'; 3 | export * from './lib/gamma-config.service'; 4 | export * from './lib/consts'; -------------------------------------------------------------------------------- /libs/gamma/src/lib/animations.ts: -------------------------------------------------------------------------------- 1 | import { animate, style, transition, trigger } from '@angular/animations'; 2 | 3 | export const SLIDEDOWN = trigger('slideDown',[ 4 | transition(':leave',[ 5 | style({ 6 | transform: 'translateY(0)', 7 | opacity:1 8 | }), 9 | animate('0.2s cubic-bezier(0.4, 0, 1, 1)',style({ 10 | opacity: 0.5, 11 | transform: 'translateY(44px)' 12 | })) 13 | ]) 14 | ]) -------------------------------------------------------------------------------- /libs/gamma/src/lib/gamma-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Optional } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { BLUE, ColorInfo, CYAN, MAGNET, ORANGE, RED, YELLOW, YELLOW_GREEN } from './consts'; 4 | import { MessageProvider, MESSAGE_PROVIDER } from './message-provider'; 5 | 6 | export interface GammaConfiguration { 7 | hideTimestamp: boolean; 8 | tickerDisplayThreshold: number; 9 | disableSmoother: boolean; 10 | maxDanmakuNumber: number; 11 | } 12 | 13 | export const DEFAULT_GAMMA_CONFIGURATION:GammaConfiguration = { 14 | hideTimestamp: false, 15 | tickerDisplayThreshold: 50, 16 | disableSmoother: false, 17 | maxDanmakuNumber: 100 18 | } 19 | 20 | @Injectable() 21 | export class GammaConfigService { 22 | 23 | //mutable :ascending order 24 | private colorInfoList = [CYAN, YELLOW_GREEN, YELLOW, ORANGE, MAGNET, RED].sort((a, b) => a.price_limit - b.price_limit); 25 | 26 | current$: BehaviorSubject = new BehaviorSubject(DEFAULT_GAMMA_CONFIGURATION); 27 | 28 | constructor(@Optional() @Inject(MESSAGE_PROVIDER) provider: MessageProvider){ 29 | provider?.registerOnConfiguration((config)=>{ 30 | // TODO: validate 31 | this.current$.next({ 32 | ...DEFAULT_GAMMA_CONFIGURATION, 33 | ...config, 34 | }); 35 | }); 36 | } 37 | 38 | getColorInfo(value: number) { 39 | let lastColorInfo:ColorInfo = BLUE; 40 | for(const info of this.colorInfoList){ 41 | if(value>=info.price_limit){ 42 | lastColorInfo = info; 43 | } else { 44 | break; 45 | } 46 | } 47 | return lastColorInfo; 48 | } 49 | 50 | setColorInfos(colorInfos: ColorInfo[]){ 51 | this.colorInfoList = colorInfos.sort((a, b) => a.price_limit - b.price_limit); 52 | } 53 | } -------------------------------------------------------------------------------- /libs/gamma/src/lib/gamma.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; 3 | import { GammaApp } from './gamma.app'; 4 | import { TextMessageRenderer } from './text-message/text-message'; 5 | import { PaidMessageRenderer } from './paid-message/paid-message'; 6 | import { TickerPaidStickerItemRenderer } from './ticker-paid-sticker/ticker-paid-sticker'; 7 | import { MembershipItemRenderer } from './membership-item/membership-item'; 8 | import { PaidStickerRenderer } from './paid-sticker/paid-sticker'; 9 | import { NzPipesModule } from 'ng-zorro-antd/pipes'; 10 | import { TickerSponsorItemRenderer } from './ticker-sponsor-item/ticker-sponsor-item'; 11 | import { TickerPaidMessageItemRenderer } from './ticker-paid-item/ticker-paid-item'; 12 | import { ReactiveComponentModule } from '@ngrx/component'; 13 | 14 | @NgModule({ 15 | declarations: [GammaApp, 16 | TextMessageRenderer, PaidMessageRenderer, MembershipItemRenderer, PaidStickerRenderer, 17 | TickerSponsorItemRenderer, TickerPaidMessageItemRenderer, TickerPaidStickerItemRenderer], 18 | imports: [ 19 | CommonModule, 20 | NzPipesModule, 21 | ReactiveComponentModule 22 | ], 23 | providers: [], 24 | exports: [GammaApp], 25 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 26 | }) 27 | export class GammaModule { 28 | 29 | } -------------------------------------------------------------------------------- /libs/gamma/src/lib/membership-item/membership-item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{message.username}} 18 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{message.itemInfo}} 31 | 32 | 33 | 34 | {{date | date:'hh:mm a'}} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/membership-item/membership-item.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { MemberMessage } from '@comen/common'; 3 | 4 | @Component({ 5 | // eslint-disable-next-line 6 | selector: 'yt-live-chat-membership-item-renderer', 7 | templateUrl: './membership-item.html', 8 | // eslint-disable-next-line 9 | host: { 10 | class: 'style-scope yt-live-chat-item-list-renderer', 11 | 'show-only-header': '' 12 | } 13 | }) 14 | // eslint-disable-next-line 15 | export class MembershipItemRenderer { 16 | 17 | @Input() message: MemberMessage; 18 | 19 | readonly date = new Date(); 20 | } 21 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/message-provider.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { GammaConfiguration } from './gamma-config.service'; 3 | import { Message } from '@comen/common'; 4 | 5 | export interface MessageProvider { 6 | registerOnMessage(callback:(message:Message)=>unknown):void; 7 | registerOnConfiguration(callback:(config:GammaConfiguration)=>unknown):void; 8 | } 9 | 10 | export const MESSAGE_PROVIDER = new InjectionToken('gamma_message_provider'); -------------------------------------------------------------------------------- /libs/gamma/src/lib/paid-message/paid-message.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | {{message.username}} 13 | 14 | 16 | 17 | 18 | 19 | {{message.itemInfo}} 20 | 21 | 22 | {{date | date:'hh:mm a'}} 23 | 24 | 25 | 26 | 27 | {{message.content}} 28 | 30 | 31 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/paid-message/paid-message.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { GammaConfigService } from '../gamma-config.service'; 3 | import { PaidMessage } from '@comen/common'; 4 | 5 | @Component({ 6 | // eslint-disable-next-line 7 | selector: 'yt-live-chat-paid-message-renderer', 8 | templateUrl: './paid-message.html', 9 | // eslint-disable-next-line 10 | host: { 11 | class: 'style-scope yt-live-chat-item-list-renderer', 12 | 'allow-animations': '', 13 | '[style]': 'colorStyle', 14 | '[attr.show-only-header]': '(message.content==\'\'||!message.content)?\'\':null' // TO BE CHECKED 15 | } 16 | }) 17 | // eslint-disable-next-line 18 | export class PaidMessageRenderer { 19 | 20 | @Input() message: PaidMessage; 21 | 22 | constructor(private config: GammaConfigService) { 23 | } 24 | 25 | get colorStyle() { 26 | const color = this.config.getColorInfo(this.message.price); 27 | return ` 28 | --yt-live-chat-paid-message-primary-color: ${color.primary}; 29 | --yt-live-chat-paid-message-secondary-color: ${color.secondary}; 30 | --yt-live-chat-paid-message-header-color: ${color.header}; 31 | --yt-live-chat-paid-message-author-name-color: ${color.authorName}; 32 | --yt-live-chat-paid-message-timestamp-color: ${color.timestamp}; 33 | --yt-live-chat-paid-message-color: ${color.message}; 34 | `; 35 | } 36 | 37 | readonly date = new Date(); 38 | } 39 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/paid-sticker/paid-sticker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | {{date | date:'hh:mm'}} 9 | 10 | 11 | {{message.username}} 13 | 15 | {{message.itemInfo}} 16 | 17 | 18 | 19 | 20 | 21 | 24 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/paid-sticker/paid-sticker.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 2 | import { GammaConfigService } from '../gamma-config.service'; 3 | import { StickerMessage } from '@comen/common'; 4 | 5 | @Component({ 6 | // eslint-disable-next-line 7 | selector: 'yt-live-chat-paid-sticker-renderer', 8 | templateUrl: './paid-sticker.html', 9 | // eslint-disable-next-line 10 | host: { 11 | class: 'style-scope yt-live-chat-item-list-renderer', 12 | '[style]': 'colorStyle' 13 | }, 14 | encapsulation: ViewEncapsulation.None 15 | }) 16 | // eslint-disable-next-line 17 | export class PaidStickerRenderer { 18 | 19 | @Input() message: StickerMessage; 20 | 21 | constructor(private config: GammaConfigService) { } 22 | 23 | get colorInfo() { 24 | return this.config.getColorInfo(this.message.price); 25 | } 26 | 27 | get colorStyle() { 28 | return `--yt-live-chat-paid-sticker-chip-background-color: ${this.colorInfo.primary}; 29 | --yt-live-chat-paid-sticker-chip-text-color: ${this.colorInfo.message}; 30 | --yt-live-chat-paid-sticker-background-color: ${this.colorInfo.secondary}; 31 | --yt-live-chat-paid-sticker-author-name-text-color: ${this.colorInfo.authorName};`; 32 | } 33 | 34 | readonly date = new Date(); 35 | } 36 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/text-message/text-message.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { GammaConfigService } from '../gamma-config.service'; 3 | import { TextMessage, RichTextMessage } from '@comen/common'; 4 | 5 | @Component({ 6 | // eslint-disable-next-line 7 | selector: 'yt-live-chat-text-message-renderer', 8 | templateUrl: './text-message.html', 9 | // eslint-disable-next-line 10 | host: { 11 | class: 'style-scope yt-live-chat-item-list-renderer', 12 | '[attr.author-type]': 'userType' 13 | } 14 | }) 15 | // eslint-disable-next-line 16 | export class TextMessageRenderer { 17 | 18 | // to disable angular template type check : type inference is not intelligent enough 19 | // eslint-disable-next-line 20 | @Input() message: TextMessage | RichTextMessage | any; 21 | 22 | readonly date = new Date(); 23 | 24 | constructor(public config:GammaConfigService) {} 25 | 26 | get userType(){ 27 | if(this.isOwner){ 28 | return 'owner'; 29 | } 30 | else if(this.isModerator){ 31 | return 'moderator'; 32 | } 33 | else if(this.isMember){ 34 | return 'member'; 35 | } else{ 36 | return ''; 37 | } 38 | } 39 | 40 | get isMember() { 41 | return (this.message.usertype & 1) === 1; 42 | } 43 | 44 | get isModerator() { 45 | return (this.message.usertype & 2) === 2; 46 | } 47 | 48 | get isOwner() { 49 | return (this.message.usertype & 4) === 4; 50 | } 51 | 52 | get isRichtext(){ 53 | return this.message.type == 'richtext'; 54 | } 55 | 56 | get richtext(){ 57 | // to disable angular template type check : type inference is not intelligent enough 58 | // eslint-disable-next-line 59 | return (this.message as RichTextMessage).richtext as any; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/ticker-paid-item/ticker-paid-item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | {{message.itemInfo}} 10 | 11 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/ticker-paid-item/ticker-paid-item.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Input, ViewChild } from '@angular/core'; 2 | import { SLIDEDOWN } from '../animations'; 3 | import { PaidMessage } from '@comen/common'; 4 | 5 | @Component({ 6 | // eslint-disable-next-line 7 | selector: 'yt-live-chat-ticker-paid-message-item-renderer', 8 | templateUrl: './ticker-paid-item.html', 9 | // eslint-disable-next-line 10 | host: { 11 | class: 'style-scope yt-live-chat-ticker-renderer', 12 | role: 'button', 13 | style: 'overflow: hidden;', 14 | '[@slideDown]':'' 15 | }, 16 | animations:[ 17 | SLIDEDOWN 18 | ] 19 | }) 20 | // eslint-disable-next-line 21 | export class TickerPaidMessageItemRenderer { 22 | 23 | @Input() message: PaidMessage; 24 | 25 | @ViewChild('container') container: ElementRef; 26 | 27 | @Input() set status( 28 | val: { 29 | primary_color: string; 30 | secondary_color: string; 31 | text_color: string; 32 | percent: number 33 | } 34 | ) { 35 | this.container?.nativeElement?.setAttribute('style', `background:linear-gradient(90deg, ${val.primary_color},${val.primary_color}` + 36 | ` ${(1 - val.percent) * 100}%,${val.secondary_color} ${(1 - val.percent) * 100}%,${val.secondary_color});`); 37 | this.container?.nativeElement?.firstElementChild.setAttribute('style', `color: ${val.text_color}`) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/ticker-paid-sticker/ticker-paid-sticker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/ticker-paid-sticker/ticker-paid-sticker.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Input, ViewChild } from '@angular/core'; 2 | import { SLIDEDOWN } from '../animations'; 3 | import { StickerMessage } from '@comen/common'; 4 | 5 | @Component({ 6 | // eslint-disable-next-line 7 | selector: 'yt-live-chat-ticker-paid-sticker-item-renderer', 8 | templateUrl: './ticker-paid-sticker.html', 9 | // eslint-disable-next-line 10 | host: { 11 | class: 'style-scope yt-live-chat-ticker-renderer', 12 | role: 'botton', 13 | '[@slideDown]':'' 14 | }, 15 | animations:[ 16 | SLIDEDOWN 17 | ] 18 | }) 19 | // eslint-disable-next-line 20 | export class TickerPaidStickerItemRenderer { 21 | 22 | @Input() message: StickerMessage; 23 | 24 | @ViewChild('container') container: ElementRef; 25 | 26 | @Input() set status( 27 | val: { 28 | primary_color: string; 29 | secondary_color: string; 30 | percent: number 31 | } 32 | ) { 33 | this.container?.nativeElement?.setAttribute('style', `background:linear-gradient(90deg, ${val.primary_color},${val.primary_color}` + 34 | ` ${(1 - val.percent) * 100}%,${val.secondary_color} ${(1 - val.percent) * 100}%,${val.secondary_color});`); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/ticker-sponsor-item/ticker-sponsor-item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 会员 9 | 10 | -------------------------------------------------------------------------------- /libs/gamma/src/lib/ticker-sponsor-item/ticker-sponsor-item.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Input, ViewChild } from '@angular/core'; 2 | import { SLIDEDOWN } from '../animations'; 3 | import { MemberMessage } from '@comen/common'; 4 | 5 | @Component({ 6 | // eslint-disable-next-line 7 | selector: 'yt-live-chat-ticker-sponsor-item-renderer', 8 | templateUrl: './ticker-sponsor-item.html', 9 | // eslint-disable-next-line 10 | host: { 11 | class: 'style-scope yt-live-chat-ticker-renderer', 12 | role: 'button', 13 | '[@slideDown]':'' 14 | }, 15 | animations:[ 16 | SLIDEDOWN 17 | ] 18 | }) 19 | // eslint-disable-next-line 20 | export class TickerSponsorItemRenderer { 21 | 22 | @Input() message: MemberMessage; 23 | 24 | @ViewChild('container') container: ElementRef; 25 | 26 | @Input() set status( 27 | val: { 28 | primary_color: string; // not used 29 | secondary_color: string; // not used 30 | percent: number 31 | } 32 | ) { 33 | this.container?.nativeElement?.setAttribute('style', `background:linear-gradient(90deg, rgba(15,157,88,1),rgba(15,157,88,1)` + 34 | ` ${(1 - val.percent) * 100}%,rgba(11,128,67,1) ${(1 - val.percent)* 100}%,rgba(11,128,67,1));`); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /libs/gamma/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /libs/gamma/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/gamma/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "lib": ["dom", "es2018"] 10 | }, 11 | "angularCompilerOptions": { 12 | "skipTemplateCodegen": true, 13 | "strictMetadataEmit": true, 14 | "enableResourceInlining": true 15 | }, 16 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 17 | "include": ["**/*.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /libs/gamma/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/isomorphic-danmaku-server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } 2 | -------------------------------------------------------------------------------- /libs/isomorphic-danmaku-server/README.md: -------------------------------------------------------------------------------- 1 | # isomorphic-danmaku-server 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test isomorphic-danmaku-server` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/isomorphic-danmaku-server/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'isomorphic-danmaku-server', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsConfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | transform: { 10 | '^.+\\.[tj]sx?$': 'ts-jest', 11 | }, 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 13 | coverageDirectory: '../../coverage/libs/isomorphic-danmaku-server', 14 | }; 15 | -------------------------------------------------------------------------------- /libs/isomorphic-danmaku-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isomorphic-danmaku-server", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "got": "^11.8.0", 7 | "tough-cookie": "^4.0.0" 8 | } 9 | } -------------------------------------------------------------------------------- /libs/isomorphic-danmaku-server/src/index.ts: -------------------------------------------------------------------------------- 1 | export { getAcfunRoomInfo } from './lib/acfun_prefetch'; 2 | export { getBilibiliRoomInfo } from './lib/bilibili_prefetch'; -------------------------------------------------------------------------------- /libs/isomorphic-danmaku-server/src/lib/acfun_prefetch.ts: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | import { Cookie, CookieJar } from 'tough-cookie'; 3 | import { promisify } from 'util'; 4 | import { AcfunGiftListResponse, AcfunRoomInfoResponse } from './types'; 5 | 6 | export async function getAcfunRoomInfo(roomId: number) { 7 | if (!roomId) { 8 | throw 'NO ROOMID PROVIDED'; 9 | } 10 | const jar = new CookieJar(); 11 | await got.get('https://live.acfun.cn/', { cookieJar: jar }); 12 | const did = (await promisify(jar.getCookies.bind(jar))('https://acfun.cn/') as Cookie[]).find(s => s.key === '_did').value; 13 | const loginResp = (await got.post('https://id.app.acfun.cn/rest/app/visitor/login/', { 14 | headers: { 15 | "content-type": "application/x-www-form-urlencoded", 16 | }, 17 | responseType: 'json', 18 | body: "sid=acfun.api.visitor", 19 | cookieJar: jar 20 | })).body as { result: number; acSecurity: string; userId: number; 'acfun.api.visitor_st': string; }; 21 | const roomInfo = (await got.post(`https://api.kuaishouzt.com/rest/zt/live/web/startPlay?subBiz=mainApp&kpn=ACFUN_APP&kpf=PC_WEB` + 22 | `&userId=${loginResp.userId}&did=${did}&acfun.api.visitor_st=${loginResp["acfun.api.visitor_st"]}`, { 23 | headers: { 24 | "content-type": "application/x-www-form-urlencoded", 25 | "referer": `https://live.acfun.cn/live/${roomId}` 26 | }, 27 | responseType: 'json', 28 | body: `authorId=${roomId}`, 29 | cookieJar: jar 30 | })).body as AcfunRoomInfoResponse; 31 | if (roomInfo.result != 1) { 32 | return { 33 | closed: true, 34 | acSecurity: loginResp.acSecurity, 35 | userId: loginResp.userId, 36 | serviceToken: loginResp["acfun.api.visitor_st"], 37 | } 38 | } 39 | const giftInfo = (await got.post(`https://api.kuaishouzt.com/rest/zt/live/web/gift/list?subBiz=mainApp&kpn`+ 40 | `=ACFUN_APP&kpf=PC_WEB&userId=${loginResp.userId}&did=${did}&acfun.api.visitor_st=${loginResp["acfun.api.visitor_st"]}`, { 41 | headers: { 42 | "content-type": "application/x-www-form-urlencoded", 43 | "referer": `https://live.acfun.cn/live/${roomId}` 44 | }, 45 | responseType: 'json', 46 | body: `visitorId=${loginResp.userId}&liveId=${roomInfo.data.liveId}`, 47 | cookieJar: jar 48 | })).body as AcfunGiftListResponse; 49 | return { 50 | closed: false, 51 | acSecurity: loginResp.acSecurity, 52 | userId: loginResp.userId, 53 | serviceToken: loginResp["acfun.api.visitor_st"], 54 | tickets: roomInfo.data.availableTickets, 55 | enterRoomAttach: roomInfo.data.enterRoomAttach, 56 | liveId: roomInfo.data.liveId, 57 | giftInfo: giftInfo.data 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /libs/isomorphic-danmaku-server/src/lib/bilibili_prefetch.ts: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | import { BilibiliDanmuInfo, BilibiliGiftConfig, BilibiliHistoryDanmaku, BilibiliRoomInfo } from 'isomorphic-danmaku'; 3 | import { BilibiliGetDanmuInfoResponse, BilibiliGetHistoryResponse, BilibiliGiftConfigResponse, BilibiliRoomInitResponse } from './types'; 4 | 5 | export async function getBilibiliRoomInfo(roomId: number, options?: { 6 | fetchGift: boolean, 7 | fetchHistoryDanmaku: boolean 8 | }) { 9 | //Todo: error control 10 | const roomInfo = (await got.get(`https://api.live.bilibili.com/room/v1/Room/room_init?id=${roomId}`, { 11 | responseType: 'json' 12 | })).body as BilibiliRoomInitResponse; 13 | 14 | const realRoomId = roomInfo.data.room_id; 15 | 16 | const danmuInfo = (await got.get(`https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=${realRoomId}&type=0`, 17 | { 18 | responseType: 'json' 19 | })).body as BilibiliGetDanmuInfoResponse; 20 | 21 | let ret: { 22 | roomInfo: BilibiliRoomInfo, 23 | danmuInfo: BilibiliDanmuInfo, 24 | giftInfo?: { 25 | list: BilibiliGiftConfig[] 26 | }, 27 | history?: { 28 | admin: BilibiliHistoryDanmaku[], 29 | room: BilibiliHistoryDanmaku[] 30 | } 31 | } = { 32 | roomInfo: roomInfo.data, 33 | danmuInfo: danmuInfo.data, 34 | }; 35 | 36 | if (options?.fetchGift) { 37 | const giftInfo = (await got.get(`https://api.live.bilibili.com/xlive/web-room/v1/giftPanel/giftConfig?platform=pc&room_id=${realRoomId}`, { 38 | responseType: 'json' 39 | })).body as BilibiliGiftConfigResponse; 40 | ret = { 41 | ...ret, 42 | giftInfo: giftInfo.data 43 | } 44 | } 45 | if (options?.fetchHistoryDanmaku) { 46 | const history = (await got.get(`https://api.live.bilibili.com/xlive/web-room/v1/dM/gethistory?roomid=${realRoomId}`, { 47 | responseType: 'json' 48 | })).body as BilibiliGetHistoryResponse; 49 | ret = { 50 | ...ret, 51 | history: history.data 52 | } 53 | } 54 | 55 | return ret; 56 | } 57 | -------------------------------------------------------------------------------- /libs/isomorphic-danmaku-server/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { BilibiliDanmuInfo, BilibiliGiftConfig, BilibiliHistoryDanmaku, BilibiliRoomInfo } from 'isomorphic-danmaku'; 2 | 3 | interface AcfunRoomInfoResponse { 4 | result: number; 5 | data: { 6 | liveId: string; 7 | availableTickets: string[]; 8 | enterRoomAttach: string; 9 | videoPlayRes: string; 10 | caption: string; 11 | ticketRetryCount: number; 12 | ticketRetryIntervalMs: number; 13 | notices: { userId: number; userName: string; userGender: string; notice: string; }[]; 14 | config: { 15 | giftSlotSize: number; 16 | }; 17 | liveStartTime: number; 18 | panoramic: boolean; 19 | }, 20 | host: string; 21 | } 22 | 23 | interface AcfunGiftListResponse { 24 | result: number; 25 | data: { 26 | giftList: { 27 | allowBatchSendSizeList: number[]; 28 | arLiveName: string; 29 | canCombo: boolean; 30 | canDraw: boolean; 31 | description: string; 32 | giftId: number; 33 | giftName: string; 34 | giftPrice: number; 35 | magicFaceId: number; 36 | payWalletType: number; 37 | pngPicListt: { 38 | cdn: string; 39 | freeTraffic: boolean; 40 | url: string; 41 | urlPattern: string; 42 | }[]; 43 | smallPngPicList: { 44 | cdn: string; 45 | freeTraffic: boolean; 46 | url: string; 47 | urlPattern: string; 48 | }[]; 49 | webpPicList: { 50 | cdn: string; 51 | freeTraffic: boolean; 52 | url: string; 53 | urlPattern: string; 54 | }[]; 55 | redpackPrice: number; 56 | }[], 57 | externalDisplayGiftId: number; 58 | giftListHash: string; 59 | externalDisplayGiftTipsDelayTime: number; 60 | }, 61 | host: string; 62 | } 63 | 64 | interface BilibiliRoomInitResponse { 65 | code: number; 66 | msg: string; 67 | message: string; 68 | data: BilibiliRoomInfo; 69 | } 70 | 71 | interface BilibiliGetDanmuInfoResponse { 72 | code: number; 73 | message: string; 74 | ttl: number; 75 | data: BilibiliDanmuInfo; 76 | } 77 | 78 | interface BilibiliGiftConfigResponse { 79 | code: number; 80 | message: string; 81 | ttl: number; 82 | data: { 83 | list: BilibiliGiftConfig[] 84 | } 85 | } 86 | 87 | interface BilibiliGetHistoryResponse { 88 | code: number; 89 | data: { 90 | admin: BilibiliHistoryDanmaku[], 91 | room: BilibiliHistoryDanmaku[] 92 | }, 93 | message: string; 94 | msg: string; 95 | } 96 | 97 | export { 98 | AcfunGiftListResponse, AcfunRoomInfoResponse, BilibiliRoomInitResponse, 99 | BilibiliGetDanmuInfoResponse, BilibiliGiftConfigResponse, BilibiliGetHistoryResponse 100 | }; -------------------------------------------------------------------------------- /libs/isomorphic-danmaku-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/isomorphic-danmaku-server/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["**/*.spec.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /libs/isomorphic-danmaku-server/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /libs/isomorphic-danmaku/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } 2 | -------------------------------------------------------------------------------- /libs/isomorphic-danmaku/README.md: -------------------------------------------------------------------------------- 1 | # 📖 isomorphic-danmaku 2 | 3 | [](https://www.npmjs.com/package/isomorphic-danmaku) 4 | [](https://github.com/3Shain/isomorphic-danmaku/LICENSE) 5 | [](http://makeapullrequest.com) 6 | 7 | browser/nodejs 同构的直播平台弹幕获取 8 | 9 | **此package仍在活跃开发阶段,接口规范尚未确定,请注意后续可能产生的breaking change** 10 | 11 | ## 💻 支持平台(字母排序) 12 | 13 | - acfun 14 | - bilibili 15 | 16 | > 暂不对数据二次封装,需自行处理消息的解析。_之后可能会有计划提出并封装为一种标准格式并提供一个 pure function pipe_ 17 | 18 | ## 📖 安装 19 | 20 | ```shell 21 | npm install isomorphic-danmaku 22 | ``` 23 | 24 | ## 🍥 食用方法 25 | 26 | ### 建立连接 27 | 28 | 1. 预获取房间信息(bilibili可省略,acfun必须) 29 | ```ts 30 | // 以esmodule导入为例子,nodejs下也可以使用require() 31 | import { getAcfunRoomInfo, getBilibiliRoomInfo } from 'isomorphic-danmaku-server'; //注意后面的-server 32 | 33 | // 获取对应房间信息 34 | 35 | const acinfo = await getAcfunRoomInfo(123456); 36 | 37 | const biliinfo = await getBilibiliRoomInfo(123456); 38 | 39 | 40 | ``` 41 | 42 | > 此步骤只能运行在nodejs上,因为浏览器无法跨域请求。如果你想在浏览器上直接连接(ws默认不禁止跨域),则需要有一个服务端为其提供这些预请求房间信息。 43 | 44 | 2. 连接房间并获取弹幕 45 | 46 | ```ts 47 | 48 | import { connectBilibiliLiveWs } from 'isomorphic-danmaku'; 49 | // 对于acfun 50 | import { connectAcfunLiveWs } from 'isomorphic-danmaku'; 51 | 52 | for await (let msg of connectBilibiliLiveWs({ roomId: 123456 })){ 53 | // 直接处理msg 54 | console.log(msg); 55 | // 跳过continue,停止处理则可使用break 56 | } 57 | 58 | ``` 59 | 60 | Acfun需要的参数信息 61 | ```ts 62 | { 63 | roomId: number; //房间号 64 | //以下字段均从上述接口获取 65 | acSecurity: string; 66 | serviceToken: string; 67 | tickets: string[]; 68 | liveId: string; 69 | userId: stirng; 70 | enterRoomAttach: string; 71 | } 72 | ``` 73 | Bilibili需要的参数信息 74 | ```ts 75 | { 76 | roomId: number; // 房间号 77 | host?: string; // 连接的服务器,是一个域名 78 | token?: string; // 可从roominfo接口获取 79 | } 80 | ``` 81 | 82 | `scripts/`文件夹内有nodejs的例子。 83 | 84 | ### 在浏览器上使用(连接ws的部分) 85 | 86 | 在 package.json 里添加以下字段 87 | 88 | ```json 89 | "browser": { 90 | "@peculiar/webcrypto": false 91 | } 92 | ``` 93 | 94 | 这是为了告诉 bundler(如 webpack,rollup 此类)忽略这些包的导入(大多是node环境独有的包)。 95 | 96 | 暂不提供 umd 打包格式,即无法直接通过添加一个`