Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add userpresence #68

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 0 additions & 33 deletions .github/workflows/ably-infrastructure.yml

This file was deleted.

14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"name": "Jest API",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand", "--watchAll=false"],
"cwd": "${workspaceFolder}/api",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
},
{
"name": "Attach to Node Functions",
"type": "node",
Expand Down
9 changes: 5 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
"azureFunctions.preDeployTask": "npm prune (functions)",
"typescript.tsdk": "node_modules\\typescript\\lib",
"prettier.printWidth": 180,
"editor.codeActionsOnSave": { "source.fixAll.eslint": true },
"editor.formatOnSave": true,
"eslint.alwaysShowStatus": true,
"editor.codeActionsOnSave": { "source.fixAll.eslint": true },
"editor.formatOnSave": true,
"eslint.alwaysShowStatus": true,
"files.autoSave": "onFocusChange",
"cSpell.words": ["vite"]
"cSpell.words": ["vite"],
"azureFunctions.projectSubpath": "api"
}
6 changes: 4 additions & 2 deletions api/common/dataaccess/CosmosDbMetadataRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ export class CosmosDbMetadataRepository implements IMetadataRepository {
return results.resources as TEntityType[];
}

public async saveOrUpdate<TEntityType extends Entity>(entity: TEntityType): Promise<void> {
public async saveOrUpdate<TEntityType extends Entity>(entity: TEntityType): Promise<TEntityType> {
const container = await this.getContainer(entity.type);
const result = await container.items.upsert(entity);
const result = await container.items.upsert<TEntityType>(entity);

if (result.statusCode !== 201) {
throw new Error(`Error saving or updating entity ${entity.id}`);
}

return result.resource as TEntityType;
}

public async delete<TEntityType extends Entity>(entity: TEntityType): Promise<boolean> {
Expand Down
4 changes: 4 additions & 0 deletions api/common/dataaccess/IMetadataRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ export type Entity = { id: string; type: string };
export interface IMetadataRepository {
getById<TEntityType extends Entity>(typeName: string, id: string): Promise<TEntityType>;
getByProperty<TEntityType extends Entity>(typeName: string, propertyName: string, value: any): Promise<TEntityType[]>;
saveOrUpdate<TEntityType extends Entity>(entity: TEntityType): Promise<TEntityType>;
delete<TEntityType extends Entity>(entity: TEntityType): Promise<boolean>;
exists(typeName: string, id: string): Promise<boolean>;
getAll<TEntityType extends Entity>(typeName: string): Promise<TEntityType[]>;
}
66 changes: 66 additions & 0 deletions api/common/metadata/Channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Entity } from "../dataaccess/IMetadataRepository";

export enum ChannelVisibility {
Private = 0,
Public = 1
}

export interface IChannel extends Entity {
id: string;
name: string;
members: string[];
description: string;
isDefault: boolean;
createdBy: string;
visibility: ChannelVisibility;
onlineCount: number;
}

export class Channel implements IChannel, Entity {
public id: string;
public readonly type: string;
public name: string;
public members: string[];
public description: string;
public isDefault: boolean;
public createdBy: string;
public visibility: ChannelVisibility;
public onlineCount: number;

constructor() {
this.type = "Channel";
}

public static fromJSON(json: any): Channel {
// Always create Ids
if (!json.id) {
json.id = Channel.createId();
}

return Object.assign(new Channel(), json);
}

public isMember(userId: string): boolean {
return this.members.includes(userId);
}

public incrementOnlineCount(): number {
this.onlineCount += 1;

return this.onlineCount;
}

public decrementOnlineCount(): number {
this.onlineCount -= 1;

return this.onlineCount;
}

private static createId(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
}
4 changes: 4 additions & 0 deletions api/common/metadata/ChannelPresenceMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type ChannelPresenceMessage = {
channelId: string;
membersOnlineCount: Number;
};
4 changes: 4 additions & 0 deletions api/common/metadata/PresenceStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum PresenceStatus {
Offline = 0,
Online = 1
}
25 changes: 25 additions & 0 deletions api/common/metadata/User.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { PresenceStatus } from "./PresenceStatus";
import { User } from "./User";

describe("User", () => {
let sut: User;
beforeEach(() => {
sut = new User();
});

it("Is online", () => {
sut.lastOnlineTimeStampUTC = new Date(2022, 2, 1, 14, 5);
const current = new Date(2022, 2, 1, 14, 6); // Current time is within 5 minute threshold of last online.
const result = sut.getOnlineStatus(current);

expect(result).toBe(PresenceStatus.Online);
});

it("Is not online", () => {
sut.lastOnlineTimeStampUTC = new Date(2022, 2, 1, 14, 5);
const current = new Date(2022, 2, 1, 14, 11); // Current time exceeds 5 minute threshold of last online.
const result = sut.getOnlineStatus(current);

expect(result).toBe(PresenceStatus.Offline);
});
});
11 changes: 11 additions & 0 deletions api/common/metadata/User.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as bcrypt from "bcrypt";
import { Entity } from "../dataaccess/IMetadataRepository";
import { PresenceStatus } from "./PresenceStatus";

export interface IUser extends Entity {
id: string;
Expand All @@ -12,6 +13,8 @@ export interface IUser extends Entity {
profileImgLargeUrl: string;
roleName: string;
email: string;
presenceStatus: PresenceStatus;
lastOnlineTimeStampUTC: Date;
}

export class User implements IUser, Entity {
Expand All @@ -27,6 +30,8 @@ export class User implements IUser, Entity {
public profileImgLargeUrl: string;
public roleName: string;
public email: string;
public presenceStatus: PresenceStatus;
public lastOnlineTimeStampUTC: Date;

constructor() {
this.type = "User";
Expand Down Expand Up @@ -57,6 +62,12 @@ export class User implements IUser, Entity {
return Object.assign(new User(), json);
}

public getOnlineStatus(currentDateTime?: Date): PresenceStatus {
const threshold = 1000 * 60 * 5; // 5 minutes
const current = currentDateTime !== undefined ? currentDateTime.valueOf() : Date.now();
return current - this.lastOnlineTimeStampUTC.valueOf() < threshold ? PresenceStatus.Online : PresenceStatus.Offline;
}

private static createId(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0,
Expand Down
7 changes: 7 additions & 0 deletions api/common/metadata/UserPresenceMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PresenceStatus } from "./PresenceStatus";

export type UserPresenceMessage = {
userId: string;
lastOnlineTimeStampUTC: Date;
presenceStatus: PresenceStatus;
};
136 changes: 136 additions & 0 deletions api/common/services/ChannelService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { addItemToDb, clearDbItems } from "../../test-helpers/FakeCosmosDbMetadataRepository";
import { Channel, ChannelVisibility } from "../metadata/Channel";
import {
ChannelDeletionRequest,
ChannelCreationRequest,
ChannelService,
RemoveChannelMemberRequest,
AddChannelMemberRequest
} from "./ChannelService";

describe("ChannelService", () => {
let sut: ChannelService;
beforeEach(() => {
sut = new ChannelService();
clearDbItems();
});

it("Can retrieve an existing Channel item by channelName.", async () => {
const channelName: string = "testChannel1";
const item1 = {
id: "123",
name: "testChannel1",
type: "Channel"
};
const item2 = {
id: "456",
name: "testChannel2",
type: "Channel"
};
addItemToDb("Channel", item1);
addItemToDb("Channel", item2);
const result = await sut.getChannelByName(channelName);

expect(result.channel.name).toBe(channelName);
});

it("Can retrieve all channels.", async () => {
const item1 = {
id: "123",
name: "testChannel1",
type: "Channel"
};
const item2 = {
id: "456",
name: "testChannel2",
type: "Channel"
};
addItemToDb("Channel", item1);
addItemToDb("Channel", item2);

const result = await sut.getAllChannels();

expect(result.channels).toHaveLength(2);
});

it("Can create a channel.", async () => {
const channelName = "testChannel1";
const item = {
id: "123",
name: channelName,
type: "Channel"
};
addItemToDb("Channel", item);

const channelRequest: ChannelCreationRequest = {
channelName: channelName,
description: "Test Channel 1 description",
createdBy: "testUser1",
visibility: ChannelVisibility.Public
};

const result = await sut.createChannel(channelRequest);

expect(result.name).toBe(channelRequest.channelName);
});

it("Can delete a channel.", async () => {
const channelId = "123";
const item = {
id: channelId,
name: "testChannel1",
type: "Channel"
};
addItemToDb("Channel", item);

const channelRequest: ChannelDeletionRequest = {
id: channelId
};

const result = await sut.deleteChannel(channelRequest);

expect(result.id).toBe(channelId);
});

it("Can remove a member.", async () => {
const channelId = "channel123";
const userId = "user123";
const item = {
id: channelId,
name: "testChannel1",
type: "Channel",
members: ["user456", userId]
};
addItemToDb("Channel", item);

const channelRequest: RemoveChannelMemberRequest = {
userId: userId,
channelId: channelId
};

const result = await sut.removeMember(channelRequest);

expect(result.members).not.toContain(userId);
});

it("Can add a member.", async () => {
const channelId = "channel123";
const userId = "user123";
const item = {
id: channelId,
name: "testChannel1",
type: "Channel",
members: ["user456"]
};
addItemToDb("Channel", item);

const channelRequest: AddChannelMemberRequest = {
userId: userId,
channelId: channelId
};

const result = await sut.addMember(channelRequest);

expect(result.members).toContain(userId);
});
});
Loading