Уведомления об обновлении приложения.
This commit is contained in:
2024-10-06 02:43:13 +04:00
parent 6ffe39a4a9
commit 2efceeaec4
13 changed files with 106 additions and 15 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "schedule-parser-next",
"version": "1.3.0",
"version": "1.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "schedule-parser-next",
"version": "1.3.0",
"version": "1.3.1",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/cache-manager": "^2.2.2",

View File

@@ -1,6 +1,6 @@
{
"name": "schedule-parser-next",
"version": "1.3.0",
"version": "1.3.1",
"description": "",
"author": "N08I40K",
"private": true,

View File

@@ -44,4 +44,6 @@ model User {
role UserRole
//
fcm FCM?
//
version String
}

View File

@@ -83,6 +83,7 @@ export class AuthService {
}),
role: signUpDto.role as UserRole,
group: group,
version: signUpDto.version ?? "1.0.0",
};
return this.usersService.create(input).then((user) => {

View File

@@ -1,4 +1,5 @@
import { configDotenv } from "dotenv";
import * as process from "node:process";
configDotenv();
@@ -12,9 +13,14 @@ export const httpsConstants = {
};
export const apiConstants = {
port: process.env.API_PORT ?? 5050,
port: +(process.env.API_PORT ?? 5050),
version: process.env.SERVER_VERSION!,
};
export const firebaseConstants = {
serviceAccountPath: process.env.FIREBASE_ACCOUNT_PATH!,
};
export const scheduleConstants = {
cacheInvalidateDelay: +(process.env.SERVER_CACHE_INVALIDATE_DELAY! ?? 5),
};

View File

@@ -1,4 +1,9 @@
import { ApiProperty, IntersectionType, PickType } from "@nestjs/swagger";
import {
ApiProperty,
IntersectionType,
PartialType,
PickType,
} from "@nestjs/swagger";
import { UserDto } from "./user.dto";
import { IsString } from "class-validator";
import { Expose } from "class-transformer";
@@ -19,6 +24,7 @@ export class SignInResDto extends PickType(UserDto, ["id", "accessToken"]) {}
export class SignUpReqDto extends IntersectionType(
SignInReqDto,
PickType(UserDto, ["role", "group"]),
PartialType(PickType(UserDto, ["version"])),
) {}
export class SignUpResDto extends SignInResDto {}

17
src/dto/fcm.dto.ts Normal file
View File

@@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsSemVer, IsUrl } from "class-validator";
export class FcmPostUpdateDto {
@ApiProperty({ example: "1.6.0", description: "Версия приложения" })
@IsSemVer()
// @Expose()
version: string;
@ApiProperty({
example: "https://download.host/app-release-1.6.0.apk",
description: "Ссылка на приложение",
})
@IsUrl()
// @Expose()
downloadLink: string;
}

View File

@@ -6,6 +6,7 @@ import {
IsMongoId,
IsObject,
IsOptional,
IsSemVer,
IsString,
MaxLength,
MinLength,
@@ -97,6 +98,11 @@ export class UserDto {
@IsOptional()
@Expose()
fcm: UserFcmDto | null;
@ApiProperty({ description: "Версия установленого приложения" })
@IsSemVer()
@Expose()
version: string;
}
export class ClientUserResDto extends OmitType(UserDto, [
@@ -104,6 +110,7 @@ export class ClientUserResDto extends OmitType(UserDto, [
"salt",
"accessToken",
"fcm",
"version",
]) {
static fromUserDto(userDto: UserDto): ClientUserResDto {
return plainToClass(ClientUserResDto, userDto, {

View File

@@ -1,4 +1,6 @@
import {
BadRequestException,
Body,
Controller,
HttpCode,
HttpStatus,
@@ -12,11 +14,13 @@ import { UserFromTokenPipe } from "../auth/auth.pipe";
import { UserDto } from "../dto/user.dto";
import { ResultDto } from "../utility/validation/class-validator.interceptor";
import { FirebaseAdminService } from "./firebase-admin.service";
import { FcmPostUpdateDto } from "../dto/fcm.dto";
import { isSemVer } from "class-validator";
@Controller("api/v1/fcm")
@UseGuards(AuthGuard)
export class FirebaseAdminController {
private readonly defaultTopics = new Set(["schedule-update"]);
private readonly defaultTopics = new Set(["schedule-update", "app-update"]);
constructor(private readonly firebaseAdminService: FirebaseAdminService) {}
@@ -38,4 +42,37 @@ export class FirebaseAdminController {
this.defaultTopics,
);
}
@Post("update-callback/:version")
@HttpCode(HttpStatus.OK)
@ResultDto(null)
async updateCallback(
@UserToken(UserFromTokenPipe) userDto: UserDto,
@Param("version") version: string,
) {
if (!isSemVer(version)) {
throw new BadRequestException(
"version must be a Semantic Versioning Specification",
);
}
await this.firebaseAdminService.updateApp(
userDto,
version,
this.defaultTopics,
);
}
@Post("post-update")
@HttpCode(HttpStatus.OK)
@ResultDto(null)
async postUpdate(@Body() postUpdateDto: FcmPostUpdateDto): Promise<void> {
await this.firebaseAdminService.sendByTopic("app-update", {
data: {
type: "app-update",
version: postUpdateDto.version,
downloadLink: postUpdateDto.downloadLink,
},
});
}
}

View File

@@ -66,6 +66,8 @@ export class FirebaseAdminService implements OnModuleInit {
const currentTopics = new Set(fcm.topics);
for (const topic of topics) {
if (!fcm.topics.includes(topic)) continue;
await this.messaging.unsubscribeFromTopic(fcm.token, topic);
currentTopics.delete(topic);
}
@@ -84,6 +86,8 @@ export class FirebaseAdminService implements OnModuleInit {
const currentTopics = new Set(fcm.topics);
for (const topic of topics) {
if (fcm.topics.includes(topic)) continue;
await this.messaging.subscribeToTopic(fcm.token, topic);
currentTopics.add(topic);
}
@@ -96,4 +100,17 @@ export class FirebaseAdminService implements OnModuleInit {
data: { fcm: fcm },
});
}
async updateApp(
userDto: UserDto,
version: string,
topics: Set<string>,
): Promise<void> {
await this.subscribe(userDto, topics).then(async (userDto) => {
await this.usersService.update({
where: { id: userDto.id },
data: { version: version },
});
});
}
}

View File

@@ -26,12 +26,12 @@ async function bootstrap() {
const swaggerConfig = new DocumentBuilder()
.setTitle("Schedule Parser")
.setDescription("Парсер расписания")
.setVersion("1.0")
.setVersion(apiConstants.version)
.build();
const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
swaggerDocument.servers = [
{
url: "http://localhost:3000",
url: `https://localhost:${apiConstants.port}`,
description: "Локальный сервер для разработки",
},
];

View File

@@ -1,8 +1,4 @@
import {
Inject,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
import {
ScheduleParser,
ScheduleParseResult,
@@ -23,6 +19,7 @@ import { cacheGetOrFill } from "../utility/cache.util";
import * as crypto from "crypto";
import { ScheduleReplacerService } from "./schedule-replacer.service";
import { FirebaseAdminService } from "../firebase-admin/firebase-admin.service";
import { scheduleConstants } from "../contants";
@Injectable()
export class ScheduleService {
@@ -57,7 +54,8 @@ export class ScheduleService {
return {
cacheHash: this.cacheHash,
cacheUpdateRequired:
(Date.now() - this.cacheUpdatedAt.valueOf()) / 1000 / 60 >= 5,
(Date.now() - this.cacheUpdatedAt.valueOf()) / 1000 / 60 >=
scheduleConstants.cacheInvalidateDelay,
lastCacheUpdate: this.cacheUpdatedAt.valueOf(),
lastScheduleUpdate: this.scheduleUpdatedAt.valueOf(),
};

View File

@@ -15,7 +15,7 @@ export class PartialValidationPipe implements PipeTransform {
this.partialValidationPipe = new ValidationPipe({
...options,
...{
skipUndefinedProperties: true,
skipUndefinedProperties: false,
skipNullValues: false,
},
});