mirror of https://github.com/immich-app/immich.git
* Render when a new asset is uploaded from WebSocket notification * Update Readmepull/29/head v0.4-dev
parent
7cc7fc0a0c
commit
c234c95880
@ -0,0 +1,113 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
|
import 'package:socket_io_client/socket_io_client.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
|
||||||
|
class WebscoketState {
|
||||||
|
final Socket? socket;
|
||||||
|
final bool isConnected;
|
||||||
|
|
||||||
|
WebscoketState({
|
||||||
|
this.socket,
|
||||||
|
required this.isConnected,
|
||||||
|
});
|
||||||
|
|
||||||
|
WebscoketState copyWith({
|
||||||
|
Socket? socket,
|
||||||
|
bool? isConnected,
|
||||||
|
}) {
|
||||||
|
return WebscoketState(
|
||||||
|
socket: socket ?? this.socket,
|
||||||
|
isConnected: isConnected ?? this.isConnected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'WebscoketState(socket: $socket, isConnected: $isConnected)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is WebscoketState && other.socket == socket && other.isConnected == isConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => socket.hashCode ^ isConnected.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebsocketNotifier extends StateNotifier<WebscoketState> {
|
||||||
|
WebsocketNotifier(this.ref) : super(WebscoketState(socket: null, isConnected: false)) {
|
||||||
|
debugPrint("Init websocket instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
var authenticationState = ref.watch(authenticationProvider);
|
||||||
|
|
||||||
|
if (authenticationState.isAuthenticated) {
|
||||||
|
var accessToken = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
|
var endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
|
try {
|
||||||
|
debugPrint("[WEBSOCKET] Attempting to connect to ws");
|
||||||
|
// Configure socket transports must be sepecified
|
||||||
|
Socket socket = io(
|
||||||
|
endpoint,
|
||||||
|
OptionBuilder()
|
||||||
|
.setTransports(['websocket'])
|
||||||
|
.enableReconnection()
|
||||||
|
.enableForceNew()
|
||||||
|
.enableForceNewConnection()
|
||||||
|
.enableAutoConnect()
|
||||||
|
.setExtraHeaders({"Authorization": "Bearer $accessToken"})
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.onConnect((_) {
|
||||||
|
debugPrint("[WEBSOCKET] Established Websocket Connection");
|
||||||
|
state = WebscoketState(isConnected: true, socket: socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.onDisconnect((_) {
|
||||||
|
debugPrint("[WEBSOCKET] Disconnect to Websocket Connection");
|
||||||
|
state = WebscoketState(isConnected: false, socket: null);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (errorMessage) {
|
||||||
|
debugPrint("Webcoket Error - $errorMessage");
|
||||||
|
state = WebscoketState(isConnected: false, socket: null);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('on_upload_success', (data) {
|
||||||
|
var jsonString = jsonDecode(data.toString());
|
||||||
|
ImmichAsset newAsset = ImmichAsset.fromMap(jsonString);
|
||||||
|
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
debugPrint("[WEBSOCKET] Attempting to disconnect");
|
||||||
|
var socket = state.socket?.disconnect();
|
||||||
|
if (socket != null) {
|
||||||
|
if (socket.disconnected) {
|
||||||
|
state = WebscoketState(isConnected: false, socket: null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebscoketState>((ref) {
|
||||||
|
return WebsocketNotifier(ref);
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,47 @@
|
|||||||
|
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
|
||||||
|
import { CommunicationService } from './communication.service';
|
||||||
|
import { Socket, Server } from 'socket.io';
|
||||||
|
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { UserEntity } from '../user/entities/user.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@WebSocketGateway()
|
||||||
|
export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||||
|
constructor(
|
||||||
|
private immichJwtService: ImmichJwtService,
|
||||||
|
|
||||||
|
@InjectRepository(UserEntity)
|
||||||
|
private userRepository: Repository<UserEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@WebSocketServer() server: Server;
|
||||||
|
|
||||||
|
handleDisconnect(client: Socket) {
|
||||||
|
client.leave(client.nsp.name);
|
||||||
|
|
||||||
|
Logger.log(`Client ${client.id} disconnected`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleConnection(client: Socket, ...args: any[]) {
|
||||||
|
Logger.log(`New websocket connection: ${client.id}`, 'NewWebSocketConnection');
|
||||||
|
const accessToken = client.handshake.headers.authorization.split(' ')[1];
|
||||||
|
const res = await this.immichJwtService.validateToken(accessToken);
|
||||||
|
|
||||||
|
if (!res.status) {
|
||||||
|
client.emit('error', 'unauthorized');
|
||||||
|
client.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.userRepository.findOne({ where: { id: res.userId } });
|
||||||
|
if (!user) {
|
||||||
|
client.emit('error', 'unauthorized');
|
||||||
|
client.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.join(user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CommunicationService } from './communication.service';
|
||||||
|
import { CommunicationGateway } from './communication.gateway';
|
||||||
|
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
|
||||||
|
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { jwtConfig } from '../../config/jwt.config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { UserEntity } from '../user/entities/user.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([UserEntity]), ImmichJwtModule, JwtModule.register(jwtConfig)],
|
||||||
|
providers: [CommunicationGateway, CommunicationService, ImmichJwtService],
|
||||||
|
exports: [CommunicationGateway],
|
||||||
|
})
|
||||||
|
export class CommunicationModule {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CommunicationService {}
|
||||||
@ -1,11 +1,15 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
|
|
||||||
app.set('trust proxy');
|
app.set('trust proxy');
|
||||||
|
|
||||||
|
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||||
|
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||||
|
import { RedisClient, createClient } from 'redis';
|
||||||
|
import { ServerOptions } from 'socket.io';
|
||||||
|
import { createAdapter } from '@socket.io/redis-adapter';
|
||||||
|
|
||||||
|
const pubClient = createClient({ url: 'redis://immich_redis:6379' });
|
||||||
|
const subClient = pubClient.duplicate();
|
||||||
|
|
||||||
|
export class RedisIoAdapter extends IoAdapter {
|
||||||
|
createIOServer(port: number, options?: ServerOptions): any {
|
||||||
|
const server = super.createIOServer(port, options);
|
||||||
|
server.adapter(createAdapter(pubClient, subClient));
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue