1
0
Fork 0

Added basic invite link functionality

master
WolverinDEV 2021-02-17 22:39:10 +01:00
parent 7a00f2471d
commit 41876e273c
6 changed files with 275 additions and 27 deletions

34
src/Action.ts Normal file
View File

@ -0,0 +1,34 @@
import {MessageCommandErrorResult} from "./Messages";
import {clientServiceLogger} from "./Logging";
export type ActionResult<T> = {
unwrap() : T;
} & ({
status: "success",
result: T
} | {
status: "error",
result: MessageCommandErrorResult
});
export function createErrorResult<T>(result: MessageCommandErrorResult) : ActionResult<T> {
return {
status: "error",
result: result,
unwrap(): T {
clientServiceLogger.logError("Tried to unwrap an action which failed: %o", result);
throw "action failed with " + result.type;
}
}
}
export function createResult<T>(result: T) : ActionResult<T> {
return {
status: "success",
result: result,
unwrap(): T {
return result;
}
}
}

View File

@ -9,6 +9,7 @@ import {
import {geoLocationProvider} from "./GeoLocation";
import {clientServiceLogger} from "./Logging";
import {ClientServiceConnection} from "./Connection";
import {Registry} from "tc-events";
export type LocalAgent = {
clientVersion: string,
@ -25,9 +26,17 @@ export interface ClientServiceConfig {
generateHostInfo() : LocalAgent;
}
export interface ClientServiceEvents {
/** Client service session has successfully be initialized */
notify_session_initialized: {},
/** The current active client service session has been closed */
notify_session_closed: {}
}
export class ClientServices {
readonly config: ClientServiceConfig;
private connection: ClientServiceConnection;
readonly events: Registry<ClientServiceEvents>;
private readonly connection: ClientServiceConnection;
private sessionInitialized: boolean;
private retryTimer: any;
@ -36,6 +45,7 @@ export class ClientServices {
private initializeLocaleId: number;
constructor(config: ClientServiceConfig) {
this.events = new Registry<ClientServiceEvents>();
this.config = config;
this.initializeAgentId = 0;
this.initializeLocaleId = 0;
@ -44,6 +54,9 @@ export class ClientServices {
this.connection = new ClientServiceConnection(5000);
this.connection.events.on("notify_state_changed", event => {
if(event.newState !== "connected") {
if(this.sessionInitialized) {
this.events.fire("notify_session_closed");
}
this.sessionInitialized = false;
return;
}
@ -60,20 +73,30 @@ export class ClientServices {
return;
}
this.sendInitializeAgent().then(undefined);
this.sendInitializeAgent().then(status => {
switch (status) {
case "aborted":
return;
case "error":
clientServiceLogger.logError("Failed to initialize session. Closing it and trying again in 60 seconds.");
this.scheduleRetry(60 * 1000);
return;
case "success":
this.sessionInitialized = true;
this.events.fire("notify_session_initialized");
return;
}
});
/* The locale does not really matter for the session so just run it async */
this.sendLocaleUpdate().then(undefined);
});
});
this.connection.events.on("notify_notify_received", event => {
switch (event.notify.type) {
case "NotifyClientsOnline":
this.handleNotifyClientsOnline(event.notify.payload);
break;
default:
return;
}
this.connection.registerNotifyHandler("NotifyClientsOnline", notify => {
clientServiceLogger.logInfo("Received user count update: %o", notify);
});
}
@ -89,6 +112,10 @@ export class ClientServices {
this.initializeLocaleId++;
}
getConnection() : ClientServiceConnection {
return this.connection;
}
private scheduleRetry(time: number) {
this.stop();
@ -97,12 +124,13 @@ export class ClientServices {
/**
* Returns as soon the result indicates that something else went wrong rather than transmitting.
* Note: This will not throw an exception!
* @param command
* @param retryInterval
*/
private async executeCommandWithRetry(command: MessageCommand, retryInterval: number) : Promise<MessageCommandResult> {
while(true) {
const result = await this.connection.executeCommand(command);
const result = await this.connection.executeMessageCommand(command);
switch (result.type) {
case "ServerInternalError":
case "CommandEnqueueError":
@ -133,7 +161,10 @@ export class ClientServices {
}
}
private async sendInitializeAgent() {
/**
* @returns `true` if the session agent has been successfully initialized.
*/
private async sendInitializeAgent() : Promise<"success" | "aborted" | "error"> {
const taskId = ++this.initializeAgentId;
const hostInfo = this.config.generateHostInfo();
@ -148,12 +179,22 @@ export class ClientServices {
if(this.initializeAgentId !== taskId) {
/* We don't want to send that stuff any more */
return;
return "aborted";
}
this.executeCommandWithRetry({ type: "SessionInitializeAgent", payload }, 2500).then(result => {
const result = await this.executeCommandWithRetry({ type: "SessionInitializeAgent", payload }, 2500);
if(this.initializeAgentId !== taskId) {
/* We don't want to send that stuff any more */
return "aborted";
}
if(result.type === "Success") {
clientServiceLogger.logTrace("Agent initialized", result);
return "success";
} else {
clientServiceLogger.logTrace("Agent initialize result: %o", result);
});
return "error";
}
}
private async sendLocaleUpdate() {
@ -173,12 +214,11 @@ export class ClientServices {
return;
}
this.connection.executeCommand({ type: "SessionUpdateLocale", payload }).then(result => {
clientServiceLogger.logTrace("Agent local update result: %o", result);
});
}
const result = await this.connection.executeCommand("SessionUpdateLocale", payload);
if(this.initializeLocaleId !== taskId) {
return;
}
private handleNotifyClientsOnline(notify: NotifyClientsOnline) {
clientServiceLogger.logInfo("Received user count update: %o", notify);
clientServiceLogger.logTrace("Agent local update result: %o", result);
}
}

View File

@ -0,0 +1,80 @@
import {ClientServices} from "./ClientService";
import {ActionResult, createErrorResult, createResult} from "./Action";
export type InviteLinkInfo = {
linkId: string,
timestampCreated: number,
timestampDeleted: number,
amountViewed: number,
amountClicked: number,
propertiesConnect: {[key: string]: string},
propertiesInfo: {[key: string]: string},
};
export class ClientServiceInvite {
private readonly handle: ClientServices;
constructor(handle: ClientServices) {
this.handle = handle;
}
async createInviteLink(connectProperties: {[key: string]: string}, infoProperties: {[key: string]: string}, createNew: boolean) : Promise<ActionResult<{ linkId: string, adminToken: string }>> {
const connection = this.handle.getConnection();
const notify = connection.catchNotify("NotifyInviteCreated");
const result = await connection.executeCommand("InviteCreate", {
new_link: createNew,
properties_connect: connectProperties,
properties_info: infoProperties
});
const notifyResult = notify();
if(result.type !== "Success") {
return createErrorResult(result);
}
if(notifyResult.status === "fail") {
return createErrorResult({ type: "GenericError", error: "failed to receive notify" });
}
return createResult({
adminToken: notifyResult.value.admin_token,
linkId: notifyResult.value.link_id
});
}
async queryInviteLink(linkId: string, registerView: boolean) : Promise<ActionResult<InviteLinkInfo>> {
const connection = this.handle.getConnection();
const notify = connection.catchNotify("NotifyInviteInfo", notify => notify.link_id === linkId);
const result = await connection.executeCommand("InviteQueryInfo", {
link_id: linkId,
register_view: registerView
});
const notifyResult = notify();
if(result.type !== "Success") {
return createErrorResult(result);
}
if(notifyResult.status === "fail") {
return createErrorResult({ type: "GenericError", error: "failed to receive notify" });
}
return createResult({
linkId: notifyResult.value.link_id,
amountClicked: notifyResult.value.amount_clicked,
amountViewed: notifyResult.value.amount_viewed,
timestampCreated: notifyResult.value.timestamp_created,
timestampDeleted: notifyResult.value.timestamp_deleted,
propertiesConnect: notifyResult.value.properties_connect,
propertiesInfo: notifyResult.value.properties_info,
});
}
}

View File

@ -12,9 +12,11 @@ type PendingCommand = {
interface ClientServiceConnectionEvents {
notify_state_changed: { oldState: ConnectionState, newState: ConnectionState },
notify_notify_received: { notify: MessageNotify }
}
type NotifyPayloadType<K extends MessageNotify["type"]> = Extract<MessageNotify, { type: K }>["payload"];
type CommandPayloadType<K extends MessageCommand["type"]> = Extract<MessageCommand, { type: K }>["payload"];
let tokenIndex = 0;
export class ClientServiceConnection {
readonly events: Registry<ClientServiceConnectionEvents>;
@ -25,6 +27,7 @@ export class ClientServiceConnection {
private connection: WebSocket;
private pendingCommands: {[key: string]: PendingCommand} = {};
private notifyHandler: {[key: string]: ((event) => void)[]} = {};
constructor(reconnectInterval: number) {
this.events = new Registry<ClientServiceConnectionEvents>();
@ -34,6 +37,7 @@ export class ClientServiceConnection {
destroy() {
this.disconnect();
this.events.destroy();
this.notifyHandler = {};
}
getState() : ConnectionState {
@ -122,7 +126,7 @@ export class ClientServiceConnection {
}
}
async executeCommand(command: MessageCommand) : Promise<MessageCommandResult> {
async executeMessageCommand(command: MessageCommand) : Promise<MessageCommandResult> {
if(this.connectionState !== "connected") {
return { type: "ConnectionClosed" };
}
@ -153,6 +157,83 @@ export class ClientServiceConnection {
});
}
async executeCommand<K extends MessageCommand["type"]>(command: K, payload: CommandPayloadType<K>) : Promise<MessageCommandResult> {
return await this.executeMessageCommand({ type: command as any, payload: payload as any });
}
registerNotifyHandler<K extends MessageNotify["type"]>(notify: K, callback: (notify: NotifyPayloadType<K>) => void) : () => void {
const handler = this.notifyHandler[notify] || (this.notifyHandler[notify] = []);
handler.push(callback);
return () => this.unregisterNotifyHandler(notify, callback as any);
}
unregisterNotifyHandler<K extends MessageNotify["type"]>(callback: (notify: NotifyPayloadType<K>) => void);
unregisterNotifyHandler<K extends MessageNotify["type"]>(notify: K, callback: (notify: NotifyPayloadType<K>) => void);
unregisterNotifyHandler(notifyOrCallback, callback?) {
if(typeof notifyOrCallback === "string") {
const handler = this.notifyHandler[notifyOrCallback];
if(!handler) {
return;
}
const index = handler.indexOf(callback);
if(index === -1) {
return;
}
handler.splice(index);
if(handler.length === 0) {
delete this.notifyHandler[notifyOrCallback];
}
} else {
for(const key of Object.keys(this.notifyHandler)) {
this.unregisterNotifyHandler(key as any, notifyOrCallback);
}
}
}
catchNotify<K extends MessageNotify["type"]>(notify: K, filter?: (value: NotifyPayloadType<K>) => boolean) : () => ({ status: "success", value: NotifyPayloadType<K> } | { status: "fail" }) {
/*
* Note:
* The current implementation allows the user to forget about the callback without causing any memory leaks.
* The memory might still leak if the registered notify never triggered.
*/
const handlers = this.notifyHandler[notify] || (this.notifyHandler[notify] = []);
const resultContainer = { result: null };
const handler = notify => {
if(filter && !filter(notify)) {
return;
}
resultContainer.result = notify;
unregisterHandler();
};
const unregisterHandler = () => {
const index = handlers.indexOf(handler);
if(index !== -1) {
handlers.remove(handler);
}
}
handlers.push(handler);
return () => {
unregisterHandler();
if(resultContainer.result === null) {
return {
status: "fail"
};
} else {
return {
status: "success",
value: resultContainer.result
};
}
}
}
private handleConnectFail() {
this.disconnect();
this.executeReconnect();
@ -195,7 +276,16 @@ export class ClientServiceConnection {
clientServiceLogger.logWarn("Received command result for unknown token: %o", data.token);
}
} else if(data.type === "Notify") {
this.events.fire("notify_notify_received", { notify: data.notify });
const handlers = this.notifyHandler[data.notify.type];
if(typeof handlers !== "undefined") {
for(const handler of [...handlers]) {
try {
handler(data.notify.payload);
} catch (error) {
clientServiceLogger.logError("Failed to invoke notify handler for %s: %o", data.notify, error);
}
}
}
} else {
clientServiceLogger.logWarn("Received message with invalid type: %o", (data as any).type);
}

View File

@ -34,6 +34,8 @@ export type MessageCommandResult =
| { type: "InviteKeyInvalid"; fields: string }
| { type: "InviteKeyNotFound" };
export type MessageCommandErrorResult = Exclude<MessageCommandResult, { type: "Success" }>;
export type MessageNotify =
| { type: "NotifyClientsOnline"; payload: NotifyClientsOnline }
| { type: "NotifyInviteCreated"; payload: NotifyInviteCreated }
@ -53,7 +55,7 @@ export type CommandSessionInitializeAgent = { session_type: ClientSessionType; p
export type CommandSessionUpdateLocale = { ip_country: string | null; selected_locale: string | null; local_timestamp: number };
export type CommandInviteQueryInfo = { link_id: string };
export type CommandInviteQueryInfo = { link_id: string, register_view: boolean };
export type CommandInviteLogAction = { click_type: number };

View File

@ -1,3 +1,5 @@
export { ClientServiceConfig, ClientServices, LocalAgent } from "./ClientService";
export { ClientServiceLogger, setClientServiceLogger } from "./Logging";
export { ClientSessionType } from "./Messages";
export { ClientSessionType } from "./Messages";
export { ClientServiceInvite, InviteActionResult, InviteLinkInfo, InviteCreateResult } from "./ClientServiceInvite";