forked from xiangwang25/whale-town-end-v2
Compare commits
4 Commits
a3341b6a3b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a3125075f | |||
| f37136d8c3 | |||
| f98bf3c493 | |||
| 6fb977ceaf |
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules
|
||||||
|
client/node_modules
|
||||||
|
client/dist
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
logs
|
||||||
|
generated
|
||||||
|
redis-data
|
||||||
|
test
|
||||||
|
docs
|
||||||
@@ -35,6 +35,14 @@ EMAIL_SECURE=true
|
|||||||
EMAIL_USER=
|
EMAIL_USER=
|
||||||
EMAIL_PASS=
|
EMAIL_PASS=
|
||||||
EMAIL_FROM=
|
EMAIL_FROM=
|
||||||
|
MAIL_PROVIDER=
|
||||||
|
NOVAMAILIO_MAIL_API_BASE=
|
||||||
|
NOVAMAILIO_MAIL_CREDENTIAL=
|
||||||
|
NOVAMAILIO_MAIL_FROM_NAME=
|
||||||
|
NOVAMAILIO_MAIL_FINGERPRINT=
|
||||||
|
NOVAMAILIO_MAIL_ORIGIN=
|
||||||
|
NOVAMAILIO_MAIL_REFERER=
|
||||||
|
NOVAMAILIO_MAIL_USER_AGENT=
|
||||||
|
|
||||||
# Zulip
|
# Zulip
|
||||||
ZULIP_CONFIG_MODE=dynamic
|
ZULIP_CONFIG_MODE=dynamic
|
||||||
|
|||||||
113
DEPLOYMENT.md
Normal file
113
DEPLOYMENT.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# WhaleTown End V2 部署
|
||||||
|
|
||||||
|
本文档覆盖 NestJS API、原生 WebSocket 服务和 React 管理端的单机部署。示例域名和目录与 `deploy/nginx` 中的模板一致,可按实际环境替换。
|
||||||
|
|
||||||
|
## 1. 环境要求
|
||||||
|
|
||||||
|
- Node.js 20 或更高版本
|
||||||
|
- pnpm 9
|
||||||
|
- MySQL 8 和 Redis 7
|
||||||
|
- PM2
|
||||||
|
- Nginx
|
||||||
|
- Python 3(仅皮肤生成功能需要)
|
||||||
|
|
||||||
|
生产目录默认为 `/var/www/whale-town-end-v2`。所有命令均在该目录执行。
|
||||||
|
|
||||||
|
## 2. 安装与配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
cp .env.production.example .env
|
||||||
|
chmod 600 .env
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑 `.env` 并至少完成以下配置:
|
||||||
|
|
||||||
|
- 为 `JWT_SECRET` 和 `ADMIN_TOKEN_SECRET` 设置独立的随机值。
|
||||||
|
- 完整设置 `DB_HOST`、`DB_PORT`、`DB_USERNAME`、`DB_PASSWORD` 和 `DB_NAME`,避免服务回退到内存存储。
|
||||||
|
- 完整设置 Redis 连接信息。
|
||||||
|
- 保持 REST API 使用 `PORT=3000`,聊天 WebSocket 使用 `WEBSOCKET_PORT=3001`。
|
||||||
|
- 使用 Zulip 时设置机器人凭据和至少 32 字节的 `ZULIP_API_KEY_ENCRYPTION_KEY`,并将 `ZULIP_DEGRADED_MODE_ENABLED` 设为 `false`。
|
||||||
|
- 不使用 Zulip 时可将 `ZULIP_DEGRADED_MODE_ENABLED` 设为 `true` 并留空 Zulip 凭据;此时 Zulip 集成和 API Key 加密存取功能不可用。
|
||||||
|
|
||||||
|
可分别生成随机密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
不要把 `.env`、生成的密钥或数据库备份提交到 Git。
|
||||||
|
|
||||||
|
## 3. 构建
|
||||||
|
|
||||||
|
构建后端:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
配置并构建管理端:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp client/.env.example client/.env.local
|
||||||
|
pnpm --filter whale-town-admin run build
|
||||||
|
```
|
||||||
|
|
||||||
|
确认 `client/.env.local` 中的 `VITE_API_BASE_URL` 指向实际后端 HTTPS 地址。该值在构建时写入管理端产物,修改后需要重新构建。
|
||||||
|
|
||||||
|
## 4. 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 start ecosystem.config.js
|
||||||
|
pm2 save
|
||||||
|
```
|
||||||
|
|
||||||
|
服务使用仓库根目录作为工作目录,并从根目录的 `.env` 加载运行配置。查看状态和日志:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 status
|
||||||
|
pm2 logs whale-town-end-v2
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 配置 Nginx
|
||||||
|
|
||||||
|
安装后端和管理端模板:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp deploy/nginx/whaletownend-v2.conf.example /etc/nginx/conf.d/whaletownend-v2.conf
|
||||||
|
sudo cp deploy/nginx/whaletown-admin-v2.conf.example /etc/nginx/conf.d/whaletown-admin-v2.conf
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
后端模板将 REST API 转发到 `3000`,将 `/game` 转发到独立的聊天 WebSocket 端口 `3001`,并为 `/location-broadcast` 和 `/ws/notice` 保留 REST 端口上的 WebSocket Upgrade。上线前还需在 Nginx 或上游代理配置 TLS。
|
||||||
|
|
||||||
|
## 6. 验收
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --fail https://whaletownend.xinghangee.icu/
|
||||||
|
curl --fail https://whaletownend.xinghangee.icu/health
|
||||||
|
curl --fail https://whaletownend.xinghangee.icu/api-docs
|
||||||
|
```
|
||||||
|
|
||||||
|
根接口应返回 `version: 2.0.0`,健康接口应返回 `status: ok`。还应分别验证以下 WebSocket 地址能够完成 `101 Switching Protocols`:
|
||||||
|
|
||||||
|
- `wss://whaletownend.xinghangee.icu/game`
|
||||||
|
- `wss://whaletownend.xinghangee.icu/location-broadcast`
|
||||||
|
- `wss://whaletownend.xinghangee.icu/ws/notice`
|
||||||
|
|
||||||
|
最后使用管理端和游戏客户端完成登录、刷新令牌、世界聊天、位置同步和通知的冒烟测试。
|
||||||
|
|
||||||
|
## 7. 更新与回滚
|
||||||
|
|
||||||
|
更新前备份 `.env` 和数据库,然后执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull --ff-only
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm run build
|
||||||
|
pnpm --filter whale-town-admin run build
|
||||||
|
pm2 reload whale-town-end-v2
|
||||||
|
```
|
||||||
|
|
||||||
|
出现问题时切回上一已验证提交,重新安装锁定依赖并构建,然后执行 `pm2 reload whale-town-end-v2`。数据库结构变更必须使用对应版本的迁移或备份恢复方案,不能只回滚应用代码。
|
||||||
47
Dockerfile
Normal file
47
Dockerfile
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
FROM node:22-bookworm-slim AS build
|
||||||
|
|
||||||
|
RUN npm install --global pnpm@9.15.4
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
COPY client/package.json ./client/package.json
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY nest-cli.json tsconfig.json tsconfig.build.json ./
|
||||||
|
COPY src ./src
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
FROM node:22-bookworm-slim AS runtime
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV SKIN_GENERATION_PYTHON=/opt/skin-generation-venv/bin/python
|
||||||
|
ENV PIP_INDEX_URL=https://mirrors.cloud.tencent.com/pypi/simple
|
||||||
|
|
||||||
|
RUN sed -i 's|deb.debian.org|mirrors.cloud.tencent.com|g' /etc/apt/sources.list.d/debian.sources \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install --yes --no-install-recommends ca-certificates python3 python3-venv \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN npm install --global pnpm@9.15.4
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements-skin-generation.txt ./
|
||||||
|
RUN python3 -m venv /opt/skin-generation-venv \
|
||||||
|
&& /opt/skin-generation-venv/bin/pip install --no-cache-dir --requirement requirements-skin-generation.txt
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
COPY client/package.json ./client/package.json
|
||||||
|
RUN pnpm install --prod --frozen-lockfile && pnpm store prune
|
||||||
|
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY scripts ./scripts
|
||||||
|
|
||||||
|
RUN mkdir -p config generated/account-assets generated/skins logs redis-data \
|
||||||
|
&& chown -R node:node /app
|
||||||
|
|
||||||
|
USER node
|
||||||
|
EXPOSE 3000 3001
|
||||||
|
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=4 \
|
||||||
|
CMD node -e "fetch('http://127.0.0.1:3000/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
|
||||||
|
|
||||||
|
CMD ["node", "dist/main.js"]
|
||||||
@@ -27,7 +27,7 @@ pnpm run build
|
|||||||
pnpm run start:prod
|
pnpm run start:prod
|
||||||
```
|
```
|
||||||
|
|
||||||
启动前至少需要在 `.env` 中设置随机的 `JWT_SECRET`、`ADMIN_TOKEN_SECRET` 和 `ZULIP_API_KEY_ENCRYPTION_KEY`。生产环境请从 `.env.production.example` 开始配置,不要直接使用示例值。
|
启动前至少需要在 `.env` 中设置随机的 `JWT_SECRET` 和 `ADMIN_TOKEN_SECRET`。启用 Zulip 时还必须设置 `ZULIP_API_KEY_ENCRYPTION_KEY`;若 `ZULIP_DEGRADED_MODE_ENABLED=true`,可以不配置 Zulip 凭据和加密密钥,但 Zulip 集成及 API Key 加密存取功能将不可用。生产环境请从 `.env.production.example` 开始配置,不要直接使用示例值。
|
||||||
|
|
||||||
API 默认监听 `3000` 端口,Swagger 地址为 `/api-docs`。
|
API 默认监听 `3000` 端口,Swagger 地址为 `/api-docs`。
|
||||||
|
|
||||||
@@ -39,6 +39,10 @@ pnpm --filter whale-town-admin run build
|
|||||||
|
|
||||||
管理端的 API 地址通过 `client/.env.local` 中的 `VITE_API_BASE_URL` 配置。
|
管理端的 API 地址通过 `client/.env.local` 中的 `VITE_API_BASE_URL` 配置。
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
生产部署、Nginx、PM2、验收和回滚步骤见 [DEPLOYMENT.md](DEPLOYMENT.md)。
|
||||||
|
|
||||||
## 安全
|
## 安全
|
||||||
|
|
||||||
仓库不包含 `.env`、访问令牌、SSH 私钥、数据库文件、Redis 数据、日志或生成资产。敏感配置必须通过部署环境注入。
|
仓库不包含 `.env`、访问令牌、SSH 私钥、数据库文件、Redis 数据、日志或生成资产。敏感配置必须通过部署环境注入。
|
||||||
|
|||||||
@@ -17,6 +17,32 @@ server {
|
|||||||
proxy_send_timeout 3600s;
|
proxy_send_timeout 3600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /location-broadcast {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /ws/notice {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
3
requirements-skin-generation.txt
Normal file
3
requirements-skin-generation.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
numpy==2.0.2
|
||||||
|
openai==2.14.0
|
||||||
|
Pillow==11.3.0
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Controller, Get, Query } from '@nestjs/common';
|
import { Controller, Get, Header, Param, Query, Res } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Response } from 'express';
|
||||||
import { RankingsService } from './rankings.service';
|
import { RankingsService } from './rankings.service';
|
||||||
import { RankingCategoryId } from './rankings.types';
|
import { RankingCategoryId } from './rankings.types';
|
||||||
|
|
||||||
@@ -8,6 +9,17 @@ import { RankingCategoryId } from './rankings.types';
|
|||||||
export class RankingsController {
|
export class RankingsController {
|
||||||
constructor(private readonly rankingsService: RankingsService) {}
|
constructor(private readonly rankingsService: RankingsService) {}
|
||||||
|
|
||||||
|
@Get('datawhale-honor/avatar/:memberId')
|
||||||
|
@Header('Cache-Control', 'public, max-age=86400, stale-while-revalidate=604800')
|
||||||
|
@ApiOperation({ summary: '代理 Datawhale 荣誉榜成员头像' })
|
||||||
|
async getDatawhaleMemberAvatar(
|
||||||
|
@Param('memberId') memberId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const avatar = await this.rankingsService.getMemberAvatar(memberId);
|
||||||
|
res.type(avatar.contentType).send(avatar.body);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('datawhale-honor')
|
@Get('datawhale-honor')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: '获取Datawhale荣誉榜',
|
summary: '获取Datawhale荣誉榜',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BadGatewayException, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
import { BadGatewayException, Injectable, Logger, NotFoundException, OnModuleInit } from '@nestjs/common';
|
||||||
import { Cron } from '@nestjs/schedule';
|
import { Cron } from '@nestjs/schedule';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +17,12 @@ const DATAWHALE_WEEKLY_COMMITS_URL = 'https://mv.datawhale.cc/data/commits_weekl
|
|||||||
const DATAWHALE_ASSET_BASE_URL = 'https://mv.datawhale.cc/';
|
const DATAWHALE_ASSET_BASE_URL = 'https://mv.datawhale.cc/';
|
||||||
const DEFAULT_CATEGORY: RankingCategoryId = 'weekly_commits';
|
const DEFAULT_CATEGORY: RankingCategoryId = 'weekly_commits';
|
||||||
const DEFAULT_LIMIT = 10;
|
const DEFAULT_LIMIT = 10;
|
||||||
|
const MAX_AVATAR_BYTES = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
export interface RankingAvatarPayload {
|
||||||
|
body: Buffer;
|
||||||
|
contentType: string;
|
||||||
|
}
|
||||||
|
|
||||||
const CATEGORIES: RankingCategory[] = [
|
const CATEGORIES: RankingCategory[] = [
|
||||||
{
|
{
|
||||||
@@ -115,6 +121,40 @@ export class RankingsService implements OnModuleInit {
|
|||||||
return this.getCachedPayload(DEFAULT_CATEGORY, DEFAULT_LIMIT);
|
return this.getCachedPayload(DEFAULT_CATEGORY, DEFAULT_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMemberAvatar(memberId: string): Promise<RankingAvatarPayload> {
|
||||||
|
const normalizedId = this.cleanString(memberId);
|
||||||
|
const member = this.members.find(item => this.cleanString(item.id) === normalizedId);
|
||||||
|
if (!member) {
|
||||||
|
throw new NotFoundException('榜单成员不存在');
|
||||||
|
}
|
||||||
|
const avatarUrl = this.sourceAvatarUrl(member);
|
||||||
|
if (!avatarUrl) {
|
||||||
|
throw new NotFoundException('榜单成员没有头像');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await axios.get<ArrayBuffer>(avatarUrl, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: 10000,
|
||||||
|
maxContentLength: MAX_AVATAR_BYTES,
|
||||||
|
maxBodyLength: MAX_AVATAR_BYTES,
|
||||||
|
});
|
||||||
|
const body = Buffer.from(response.data);
|
||||||
|
if (body.length === 0 || body.length > MAX_AVATAR_BYTES) {
|
||||||
|
throw new BadGatewayException('头像源返回的文件大小异常');
|
||||||
|
}
|
||||||
|
const contentType = this.detectImageContentType(body);
|
||||||
|
if (!contentType) {
|
||||||
|
throw new BadGatewayException('头像源返回了不支持的文件类型');
|
||||||
|
}
|
||||||
|
return { body, contentType };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof BadGatewayException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new BadGatewayException(`头像源请求失败:${this.errorMessage(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getCachedPayload(
|
private getCachedPayload(
|
||||||
category: RankingCategoryId,
|
category: RankingCategoryId,
|
||||||
limit: number,
|
limit: number,
|
||||||
@@ -366,6 +406,13 @@ export class RankingsService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private avatarUrl(member: DatawhaleMemberRow): string {
|
private avatarUrl(member: DatawhaleMemberRow): string {
|
||||||
|
const id = this.cleanString(member.id);
|
||||||
|
return id && this.sourceAvatarUrl(member)
|
||||||
|
? `/api/rankings/datawhale-honor/avatar/${encodeURIComponent(id)}`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private sourceAvatarUrl(member: DatawhaleMemberRow): string {
|
||||||
const avatar = this.cleanString(member.avatar);
|
const avatar = this.cleanString(member.avatar);
|
||||||
if (!avatar) {
|
if (!avatar) {
|
||||||
return '';
|
return '';
|
||||||
@@ -423,6 +470,19 @@ export class RankingsService implements OnModuleInit {
|
|||||||
return Number.isFinite(parsed) ? parsed : 0;
|
return Number.isFinite(parsed) ? parsed : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private detectImageContentType(body: Buffer): string {
|
||||||
|
if (body.length >= 8 && body.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
|
||||||
|
return 'image/png';
|
||||||
|
}
|
||||||
|
if (body.length >= 3 && body[0] === 0xff && body[1] === 0xd8 && body[2] === 0xff) {
|
||||||
|
return 'image/jpeg';
|
||||||
|
}
|
||||||
|
if (body.length >= 12 && body.toString('ascii', 0, 4) === 'RIFF' && body.toString('ascii', 8, 12) === 'WEBP') {
|
||||||
|
return 'image/webp';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
private cleanString(value: unknown): string {
|
private cleanString(value: unknown): string {
|
||||||
return String(value ?? '').trim();
|
return String(value ?? '').trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ export class ConfigManagerService implements OnModuleDestroy {
|
|||||||
zulipBotEmail: process.env.ZULIP_BOT_EMAIL || 'bot@example.com',
|
zulipBotEmail: process.env.ZULIP_BOT_EMAIL || 'bot@example.com',
|
||||||
zulipBotApiKey: process.env.ZULIP_BOT_API_KEY || '',
|
zulipBotApiKey: process.env.ZULIP_BOT_API_KEY || '',
|
||||||
|
|
||||||
websocketPort: parseInt(process.env.WEBSOCKET_PORT || '3000', 10),
|
websocketPort: parseInt(process.env.WEBSOCKET_PORT || '3001', 10),
|
||||||
websocketNamespace: process.env.WEBSOCKET_NAMESPACE || '/game',
|
websocketNamespace: process.env.WEBSOCKET_NAMESPACE || '/game',
|
||||||
|
|
||||||
messageRateLimit: parseInt(process.env.MESSAGE_RATE_LIMIT || '10', 10),
|
messageRateLimit: parseInt(process.env.MESSAGE_RATE_LIMIT || '10', 10),
|
||||||
@@ -1189,7 +1189,7 @@ export class ConfigManagerService implements OnModuleDestroy {
|
|||||||
zulipServerUrl: 'https://your-zulip-server.com',
|
zulipServerUrl: 'https://your-zulip-server.com',
|
||||||
zulipBotEmail: 'bot@example.com',
|
zulipBotEmail: 'bot@example.com',
|
||||||
zulipBotApiKey: '',
|
zulipBotApiKey: '',
|
||||||
websocketPort: 3000,
|
websocketPort: 3001,
|
||||||
websocketNamespace: '/game',
|
websocketNamespace: '/game',
|
||||||
messageRateLimit: 10,
|
messageRateLimit: 10,
|
||||||
messageMaxLength: 1000,
|
messageMaxLength: 1000,
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export const DEFAULT_ZULIP_CONFIG: ZulipConfiguration = {
|
|||||||
botApiKey: '',
|
botApiKey: '',
|
||||||
},
|
},
|
||||||
websocket: {
|
websocket: {
|
||||||
port: 3000,
|
port: 3001,
|
||||||
namespace: '/game',
|
namespace: '/game',
|
||||||
pingInterval: 25000,
|
pingInterval: 25000,
|
||||||
pingTimeout: 5000,
|
pingTimeout: 5000,
|
||||||
|
|||||||
Reference in New Issue
Block a user