454 lines
19 KiB
TypeScript
454 lines
19 KiB
TypeScript
import * as log from "../log";
|
|
import {LogCategory} from "../log";
|
|
import * as ipc from "../ipc/BrowserIPC";
|
|
import {ChannelMessage} from "../ipc/BrowserIPC";
|
|
import * as loader from "tc-loader";
|
|
import {Stage} from "tc-loader";
|
|
import {image_type, ImageCache, media_image_type} from "../file/ImageCache";
|
|
import {FileManager} from "../file/FileManager";
|
|
import {
|
|
FileDownloadTransfer,
|
|
FileTransferState,
|
|
ResponseTransferTarget,
|
|
TransferProvider,
|
|
TransferTargetType
|
|
} from "../file/Transfer";
|
|
import {CommandResult} from "../connection/ServerConnectionDeclaration";
|
|
import {ClientEntry} from "../tree/Client";
|
|
import {tr} from "../i18n/localize";
|
|
import {
|
|
AbstractAvatarManager,
|
|
AbstractAvatarManagerFactory,
|
|
AvatarState,
|
|
AvatarStateData,
|
|
ClientAvatar,
|
|
kIPCAvatarChannel,
|
|
setGlobalAvatarManagerFactory,
|
|
uniqueId2AvatarId
|
|
} from "../file/Avatars";
|
|
import {IPCChannel} from "../ipc/BrowserIPC";
|
|
import {ConnectionHandler} from "../ConnectionHandler";
|
|
import {ErrorCode} from "../connection/ErrorCode";
|
|
import {server_connections} from "tc-shared/ConnectionManager";
|
|
|
|
/* FIXME: Retry avatar download after some time! */
|
|
|
|
class LocalClientAvatar extends ClientAvatar {
|
|
protected destroyStateData(state: AvatarState, data: AvatarStateData[AvatarState]) {
|
|
if(state === "loaded") {
|
|
const tdata = data as AvatarStateData["loaded"];
|
|
URL.revokeObjectURL(tdata.url);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class AvatarManager extends AbstractAvatarManager {
|
|
handle: FileManager;
|
|
private static cache: ImageCache;
|
|
|
|
|
|
private cachedAvatars: {[avatarId: string]: LocalClientAvatar} = {};
|
|
constructor(handle: FileManager) {
|
|
super();
|
|
this.handle = handle;
|
|
|
|
if(!AvatarManager.cache)
|
|
AvatarManager.cache = new ImageCache("avatars");
|
|
}
|
|
|
|
destroy() {
|
|
Object.values(this.cachedAvatars).forEach(e => e.destroy());
|
|
this.cachedAvatars = {};
|
|
}
|
|
|
|
create_avatar_download(client_avatar_id: string) : FileDownloadTransfer {
|
|
log.debug(LogCategory.GENERAL, "Requesting download for avatar %s", client_avatar_id);
|
|
|
|
return this.handle.initializeFileDownload({
|
|
path: "",
|
|
name: "/avatar_" + client_avatar_id,
|
|
targetSupplier: async () => await TransferProvider.provider().createResponseTarget()
|
|
});
|
|
}
|
|
|
|
private async executeAvatarLoad0(avatar: LocalClientAvatar) {
|
|
if(avatar.getAvatarHash() === "") {
|
|
avatar.setUnset();
|
|
return;
|
|
}
|
|
|
|
let initialAvatarHash = avatar.getAvatarHash();
|
|
let avatarResponse: Response;
|
|
|
|
/* try to lookup our cache for the avatar */
|
|
cache_lookup: {
|
|
if(!AvatarManager.cache.setupped()) {
|
|
await AvatarManager.cache.setup();
|
|
}
|
|
|
|
const response = await AvatarManager.cache.resolve_cached('avatar_' + avatar.clientAvatarId); //TODO age!
|
|
if(!response) {
|
|
break cache_lookup;
|
|
}
|
|
|
|
let cachedAvatarHash = response.headers.has("X-avatar-version") ? response.headers.get("X-avatar-version") : undefined;
|
|
if(avatar.getAvatarHash() !== "unknown") {
|
|
if(cachedAvatarHash === undefined) {
|
|
log.debug(LogCategory.FILE_TRANSFER, tr("Invalidating cached avatar for %s (Version miss match. Cached: unset, Current: %s)"), avatar.clientAvatarId, avatar.getAvatarHash());
|
|
await AvatarManager.cache.delete('avatar_' + avatar.clientAvatarId);
|
|
break cache_lookup;
|
|
} else if(cachedAvatarHash !== avatar.getAvatarHash()) {
|
|
log.debug(LogCategory.FILE_TRANSFER, tr("Invalidating cached avatar for %s (Version miss match. Cached: %s, Current: %s)"), avatar.clientAvatarId, cachedAvatarHash, avatar.getAvatarHash());
|
|
await AvatarManager.cache.delete('avatar_' + avatar.clientAvatarId);
|
|
break cache_lookup;
|
|
}
|
|
} else if(cachedAvatarHash) {
|
|
avatar.events.fire("avatar_changed", { newAvatarHash: cachedAvatarHash });
|
|
initialAvatarHash = cachedAvatarHash;
|
|
}
|
|
|
|
avatarResponse = response;
|
|
}
|
|
|
|
/* load the avatar from the server */
|
|
if(!avatarResponse) {
|
|
let transfer = this.create_avatar_download(avatar.clientAvatarId);
|
|
|
|
try {
|
|
await transfer.awaitFinished();
|
|
|
|
if(transfer.transferState() === FileTransferState.CANCELED) {
|
|
throw tr("download canceled");
|
|
} else if(transfer.transferState() === FileTransferState.ERRORED) {
|
|
throw transfer.currentError();
|
|
} else if(transfer.transferState() !== FileTransferState.FINISHED) {
|
|
throw tr("unknown transfer finished state");
|
|
}
|
|
} catch(error) {
|
|
if(typeof error === "object" && 'error' in error && error.error === "initialize") {
|
|
const commandResult = error.commandResult;
|
|
if(commandResult instanceof CommandResult) {
|
|
if(commandResult.id === ErrorCode.FILE_NOT_FOUND) {
|
|
if(avatar.getAvatarHash() !== initialAvatarHash) {
|
|
log.debug(LogCategory.GENERAL, tr("Ignoring avatar not found since the avatar itself got updated. Out version: %s, current version: %s"), initialAvatarHash, avatar.getAvatarHash());
|
|
return;
|
|
}
|
|
|
|
avatar.setUnset();
|
|
return;
|
|
} else if(commandResult.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
|
throw tr("No permissions to download the avatar");
|
|
} else {
|
|
throw commandResult.message + (commandResult.extra_message ? " (" + commandResult.extra_message + ")" : "");
|
|
}
|
|
}
|
|
}
|
|
|
|
log.error(LogCategory.CLIENT, tr("Could not request download for avatar %s: %o"), avatar.clientAvatarId, error);
|
|
if(error === transfer.currentError())
|
|
throw transfer.currentErrorMessage();
|
|
|
|
throw typeof error === "string" ? error : tr("Avatar download failed");
|
|
}
|
|
|
|
/* could only be tested here, because before we don't know which target we have */
|
|
if(transfer.target.type !== TransferTargetType.RESPONSE)
|
|
throw "unsupported transfer target";
|
|
|
|
const transferResponse = transfer.target as ResponseTransferTarget;
|
|
if(!transferResponse.hasResponse()) {
|
|
throw tr("Avatar transfer has no response");
|
|
}
|
|
|
|
const headers = transferResponse.getResponse().headers;
|
|
if(!headers.has("X-media-bytes")) {
|
|
throw tr("Avatar response missing media bytes");
|
|
}
|
|
|
|
const type = image_type(headers.get('X-media-bytes'));
|
|
const media = media_image_type(type);
|
|
|
|
if(avatar.getAvatarHash() !== initialAvatarHash) {
|
|
log.debug(LogCategory.GENERAL, tr("Ignoring avatar not found since the avatar itself got updated. Out version: %s, current version: %s"), initialAvatarHash, avatar.getAvatarHash());
|
|
return;
|
|
}
|
|
|
|
await AvatarManager.cache.put_cache('avatar_' + avatar.clientAvatarId, transferResponse.getResponse().clone(), "image/" + media, {
|
|
"X-avatar-version": avatar.getAvatarHash()
|
|
});
|
|
|
|
avatarResponse = transferResponse.getResponse();
|
|
}
|
|
|
|
if(!avatarResponse) {
|
|
throw tr("Missing avatar response");
|
|
}
|
|
|
|
/* get an url from the response */
|
|
{
|
|
if(!avatarResponse.headers.has('X-media-bytes'))
|
|
throw "missing media bytes";
|
|
|
|
const type = image_type(avatarResponse.headers.get('X-media-bytes'));
|
|
const media = media_image_type(type);
|
|
|
|
const blob = await avatarResponse.blob();
|
|
|
|
/* ensure we're still up to date */
|
|
if(avatar.getAvatarHash() !== initialAvatarHash) {
|
|
log.debug(LogCategory.GENERAL, tr("Ignoring avatar not found since the avatar itself got updated. Out version: %s, current version: %s"), initialAvatarHash, avatar.getAvatarHash());
|
|
return;
|
|
}
|
|
|
|
if(blob.type !== "image/" + media) {
|
|
avatar.setLoaded({ url: URL.createObjectURL(blob.slice(0, blob.size, "image/" + media)) });
|
|
} else {
|
|
avatar.setLoaded({ url: URL.createObjectURL(blob) });
|
|
}
|
|
}
|
|
}
|
|
|
|
private executeAvatarLoad(avatar: LocalClientAvatar) {
|
|
const avatarHash = avatar.getAvatarHash();
|
|
|
|
avatar.setLoading();
|
|
avatar.loadingTimestamp = Date.now();
|
|
this.executeAvatarLoad0(avatar).catch(error => {
|
|
if(avatar.getAvatarHash() !== avatarHash) {
|
|
log.debug(LogCategory.GENERAL, tr("Ignoring avatar not found since the avatar itself got updated. Out version: %s, current version: %s"), avatarHash, avatar.getAvatarHash());
|
|
return;
|
|
}
|
|
|
|
if(typeof error === "string") {
|
|
avatar.setErrored({ message: error });
|
|
} else if(error instanceof Error) {
|
|
avatar.setErrored({ message: error.message });
|
|
} else {
|
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to load avatar %s (hash: %s): %o"), avatar.clientAvatarId, avatarHash, error);
|
|
avatar.setErrored({ message: tr("lookup the console") });
|
|
}
|
|
});
|
|
}
|
|
|
|
updateCache(clientAvatarId: string, clientAvatarHash: string) {
|
|
AvatarManager.cache.setup().then(async () => {
|
|
const cached = this.cachedAvatars[clientAvatarId];
|
|
if(cached) {
|
|
if(cached.getAvatarHash() === clientAvatarHash)
|
|
return;
|
|
|
|
log.info(LogCategory.GENERAL, tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), cached.getAvatarHash(), clientAvatarHash);
|
|
}
|
|
|
|
const response = await AvatarManager.cache.resolve_cached('avatar_' + clientAvatarId);
|
|
if(response) {
|
|
let cachedAvatarHash = response.headers.has("X-avatar-version") ? response.headers.get("X-avatar-version") : undefined;
|
|
if(cachedAvatarHash !== clientAvatarHash) {
|
|
await AvatarManager.cache.delete("avatar_" + clientAvatarId).catch(error => {
|
|
log.warn(LogCategory.FILE_TRANSFER, tr("Failed to delete avatar %s: %o"), clientAvatarId, error);
|
|
});
|
|
}
|
|
}
|
|
|
|
if(cached) {
|
|
cached.events.fire("avatar_changed", { newAvatarHash: clientAvatarHash });
|
|
this.executeAvatarLoad(cached);
|
|
}
|
|
});
|
|
}
|
|
|
|
resolveAvatar(clientAvatarId: string, avatarHash?: string, cacheOnly?: boolean) : ClientAvatar {
|
|
let avatar = this.cachedAvatars[clientAvatarId];
|
|
if(!avatar) {
|
|
if(cacheOnly)
|
|
return undefined;
|
|
|
|
avatar = new LocalClientAvatar(clientAvatarId);
|
|
this.cachedAvatars[clientAvatarId] = avatar;
|
|
} else if(typeof avatarHash !== "string" || avatar.getAvatarHash() === avatarHash) {
|
|
return avatar;
|
|
}
|
|
|
|
avatar.events.fire("avatar_changed", { newAvatarHash: typeof avatarHash === "string" ? avatarHash : "unknown" });
|
|
this.executeAvatarLoad(avatar);
|
|
|
|
return avatar;
|
|
}
|
|
|
|
resolveClientAvatar(client: { id?: number, database_id?: number, clientUniqueId: string }) {
|
|
let clientHandle: ClientEntry;
|
|
if(typeof client.id === "number") {
|
|
clientHandle = this.handle.connectionHandler.channelTree.findClient(client.id);
|
|
if(clientHandle?.properties.client_unique_identifier !== client.clientUniqueId)
|
|
clientHandle = undefined;
|
|
}
|
|
|
|
if(!clientHandle && typeof client.database_id === "number") {
|
|
clientHandle = this.handle.connectionHandler.channelTree.find_client_by_dbid(client.database_id);
|
|
if(clientHandle?.properties.client_unique_identifier !== client.clientUniqueId)
|
|
clientHandle = undefined;
|
|
}
|
|
|
|
return this.resolveAvatar(uniqueId2AvatarId(client.clientUniqueId), clientHandle?.properties.client_flag_avatar);
|
|
}
|
|
|
|
private static generate_default_image() : JQuery {
|
|
return $.spawn("img").attr("src", "img/style/avatar.png").css({width: '100%', height: '100%'});
|
|
}
|
|
|
|
generate_chat_tag(client: { id?: number; database_id?: number; }, client_unique_id: string, callback_loaded?: (successfully: boolean, error?: any) => any) : JQuery {
|
|
let client_handle;
|
|
if(typeof(client.id) == "number") {
|
|
client_handle = this.handle.connectionHandler.channelTree.findClient(client.id);
|
|
}
|
|
|
|
if(!client_handle && typeof(client.id) == "number") {
|
|
client_handle = this.handle.connectionHandler.channelTree.find_client_by_dbid(client.database_id);
|
|
}
|
|
|
|
if(client_handle && client_handle.clientUid() !== client_unique_id) {
|
|
client_handle = undefined;
|
|
}
|
|
|
|
const container = $.spawn("div").addClass("avatar");
|
|
if(client_handle && !client_handle.properties.client_flag_avatar)
|
|
return container.append(AvatarManager.generate_default_image());
|
|
|
|
|
|
const clientAvatarId = client_handle ? client_handle.avatarId() : uniqueId2AvatarId(client_unique_id);
|
|
if(clientAvatarId) {
|
|
const avatar = this.resolveAvatar(clientAvatarId, client_handle?.properties.client_flag_avatar);
|
|
|
|
|
|
const updateJQueryTag = () => {
|
|
const image = $.spawn("img").attr("src", avatar.getAvatarUrl()).css({width: '100%', height: '100%'});
|
|
container.append(image);
|
|
};
|
|
|
|
if(avatar.getState() !== "loading") {
|
|
/* Test if we're may able to load the client avatar sync without a loading screen */
|
|
updateJQueryTag();
|
|
return container;
|
|
}
|
|
|
|
const image_loading = $.spawn("img").attr("src", "img/loading_image.svg").css({width: '100%', height: '100%'});
|
|
|
|
/* lets actually load the avatar */
|
|
avatar.awaitLoaded().then(updateJQueryTag);
|
|
image_loading.appendTo(container);
|
|
} else {
|
|
AvatarManager.generate_default_image().appendTo(container);
|
|
}
|
|
|
|
return container;
|
|
}
|
|
|
|
flush_cache() {
|
|
this.destroy();
|
|
}
|
|
}
|
|
(window as any).flush_avatar_cache = async () => {
|
|
server_connections.all_connections().forEach(e => {
|
|
e.fileManager.avatars.flush_cache();
|
|
});
|
|
};
|
|
|
|
/* FIXME: unsubscribe if the other client isn't alive any mnore */
|
|
class LocalAvatarManagerFactory extends AbstractAvatarManagerFactory {
|
|
private ipcChannel: IPCChannel;
|
|
|
|
private subscribedAvatars: {[key: string]: { avatar: ClientAvatar, remoteAvatarId: string, unregisterCallback: () => void }[]} = {};
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.ipcChannel = ipc.getInstance().createChannel(undefined, kIPCAvatarChannel);
|
|
this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this);
|
|
|
|
server_connections.events().on("notify_handler_created", event => this.handleHandlerCreated(event.handler));
|
|
server_connections.events().on("notify_handler_deleted", event => this.handleHandlerDestroyed(event.handler));
|
|
}
|
|
|
|
getManager(handlerId: string): AbstractAvatarManager {
|
|
return server_connections.findConnection(handlerId)?.fileManager.avatars;
|
|
}
|
|
|
|
hasManager(handlerId: string): boolean {
|
|
return this.getManager(handlerId) !== undefined;
|
|
}
|
|
|
|
private handleHandlerCreated(handler: ConnectionHandler) {
|
|
this.ipcChannel.sendMessage("notify-handler-created", { handler: handler.handlerId });
|
|
}
|
|
|
|
private handleHandlerDestroyed(handler: ConnectionHandler) {
|
|
this.ipcChannel.sendMessage("notify-handler-destroyed", { handler: handler.handlerId });
|
|
const subscriptions = this.subscribedAvatars[handler.handlerId] || [];
|
|
delete this.subscribedAvatars[handler.handlerId];
|
|
|
|
subscriptions.forEach(e => e.unregisterCallback());
|
|
}
|
|
|
|
private handleIpcMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
|
|
if(broadcast)
|
|
return;
|
|
|
|
if(message.type === "query-handlers") {
|
|
this.ipcChannel.sendMessage("notify-handlers", {
|
|
handlers: server_connections.all_connections().map(e => e.handlerId)
|
|
}, remoteId);
|
|
return;
|
|
} else if(message.type === "load-avatar") {
|
|
const sendResponse = properties => {
|
|
this.ipcChannel.sendMessage("load-avatar-result", {
|
|
avatarId: message.data.avatarId,
|
|
handlerId: message.data.handlerId,
|
|
...properties
|
|
}, remoteId);
|
|
};
|
|
|
|
const avatarId = message.data.avatarId;
|
|
const handlerId = message.data.handlerId;
|
|
const manager = this.getManager(handlerId);
|
|
if(!manager) {
|
|
sendResponse({ success: false, message: tr("Invalid handler") });
|
|
return;
|
|
}
|
|
|
|
let avatar: ClientAvatar;
|
|
if(message.data.keyType === "client") {
|
|
avatar = manager.resolveClientAvatar({
|
|
id: message.data.clientId,
|
|
clientUniqueId: message.data.clientUniqueId,
|
|
database_id: message.data.clientDatabaseId
|
|
});
|
|
} else {
|
|
avatar = manager.resolveAvatar(message.data.clientAvatarId, message.data.avatarVersion);
|
|
}
|
|
|
|
const subscribedAvatars = this.subscribedAvatars[handlerId] || (this.subscribedAvatars[handlerId] = []);
|
|
const oldSubscribedAvatarIndex = subscribedAvatars.findIndex(e => e.remoteAvatarId === avatarId);
|
|
if(oldSubscribedAvatarIndex !== -1) {
|
|
const [ subscription ] = subscribedAvatars.splice(oldSubscribedAvatarIndex, 1);
|
|
subscription.unregisterCallback();
|
|
}
|
|
subscribedAvatars.push({
|
|
avatar: avatar,
|
|
remoteAvatarId: avatarId,
|
|
unregisterCallback: avatar.events.onAll(event => {
|
|
this.ipcChannel.sendMessage("avatar-event", { handlerId: handlerId, avatarId: avatarId, event: event }, remoteId);
|
|
})
|
|
});
|
|
|
|
sendResponse({ success: true, state: avatar.getState(), stateData: avatar.getStateData(), hash: avatar.getAvatarHash() });
|
|
}
|
|
}
|
|
}
|
|
|
|
loader.register_task(Stage.LOADED, {
|
|
name: "Avatar init",
|
|
function: async () => {
|
|
setGlobalAvatarManagerFactory(new LocalAvatarManagerFactory());
|
|
},
|
|
priority: 5
|
|
}); |