From 667ed437921ea66d73a1f4e39e32f6c76ea64dc9 Mon Sep 17 00:00:00 2001 From: Chegele Date: Tue, 27 Jun 2023 17:13:32 -0400 Subject: [PATCH] progress on reputation service --- src/services/reputation/graph/edge.ts | 32 +++++ src/services/reputation/graph/graph.ts | 55 +++++++++ src/services/reputation/graph/vertex.ts | 39 ++++++ src/services/reputation/reputation.service.ts | 4 + .../reputation/reputation/citation.ts | 23 ++++ src/services/reputation/reputation/member.ts | 65 ++++++++++ src/services/reputation/reputation/post.ts | 112 ++++++++++++++++++ 7 files changed, 330 insertions(+) create mode 100644 src/services/reputation/graph/edge.ts create mode 100644 src/services/reputation/graph/graph.ts create mode 100644 src/services/reputation/graph/vertex.ts create mode 100644 src/services/reputation/reputation.service.ts create mode 100644 src/services/reputation/reputation/citation.ts create mode 100644 src/services/reputation/reputation/member.ts create mode 100644 src/services/reputation/reputation/post.ts diff --git a/src/services/reputation/graph/edge.ts b/src/services/reputation/graph/edge.ts new file mode 100644 index 0000000..f7f629f --- /dev/null +++ b/src/services/reputation/graph/edge.ts @@ -0,0 +1,32 @@ +import { randomUUID } from "crypto"; +import { Vertex } from "./vertex"; + +export class Edge { + + private id: string; + private category: string; + private directional: boolean; + private parentVertex: Vertex; + private childVertex: Vertex; + + constructor(id: null | string, parent: Vertex, child: Vertex, category = "generic", directional = false) { + this.id ? id : randomUUID(); + this.parentVertex = parent; + this.childVertex = child; + this.category = category; + this.directional = directional; + } + + public getId() { return this.id; } + public getCategory() { return this.category; } + public getDirectional() { return this.directional; } + public getParentVertex() { return this.parentVertex; } + public getChildVertex() { return this.childVertex; } + + public getAdjacent(vertex: Vertex) { + if (vertex == this.childVertex) return this.parentVertex; + else if (vertex == this.parentVertex) return this.childVertex; + else throw new Error(`Edge ${this.id} is not connected to vertex ${vertex.getId()}`); + } + +} \ No newline at end of file diff --git a/src/services/reputation/graph/graph.ts b/src/services/reputation/graph/graph.ts new file mode 100644 index 0000000..a003105 --- /dev/null +++ b/src/services/reputation/graph/graph.ts @@ -0,0 +1,55 @@ +import { Edge } from "./edge"; +import { Vertex } from "./vertex"; + +export class Graph { + + private vertices: Map; + private edges: Map; + + constructor(vertices: Vertex[] = [], edges: Edge[] = []) { + this.vertices = new Map(); + this.edges = new Map(); + vertices.forEach(this.addVertex); + edges.forEach(this.addEdge); + } + + public getAllVertices() { + return Array.from(this.vertices.values()); + } + + public getVertex(id: string) { + return this.vertices.get(id); + } + + public addVertex(vertex: Vertex) { + this.vertices.set(vertex.getId(), vertex); + } + + public deleteVertex(vertex: string | Vertex) { + if (vertex instanceof Vertex) vertex = vertex.getId(); + this.vertices.delete(vertex); + } + + public getAllEdges() { + return Array.from(this.edges.values()); + } + + public getEdge(id: string) { + return this.edges.get(id); + } + + public addEdge(edge: Edge) { + edge.getParentVertex().addEdge(edge); + edge.getChildVertex().addEdge(edge); + this.edges.set(edge.getId(), edge); + } + + public deleteEdge(edge: string | Edge) { + if (typeof edge == "string") edge = this.edges.get(edge); + if (!edge) throw new Error(`Failed to identify edge with an ID of ${edge.getId()}.`); + edge.getParentVertex().removeEdge(edge); + edge.getChildVertex().removeEdge(edge); + this.edges.delete(edge.getId()); + } + +} \ No newline at end of file diff --git a/src/services/reputation/graph/vertex.ts b/src/services/reputation/graph/vertex.ts new file mode 100644 index 0000000..014662f --- /dev/null +++ b/src/services/reputation/graph/vertex.ts @@ -0,0 +1,39 @@ +import { randomUUID } from "crypto"; +import { Edge } from "./edge"; + +export class Vertex { + + private id: string + private degree: number; + private edges: Edge[]; + private data: any; + + constructor(id: null | string, data: any, edges: Edge[] = []) { + this.id = id ? id : randomUUID(); + this.data = data; + this.degree = edges.length; + this.edges = edges; + } + + public getId() { return this.id; } + public getDegree() { return this.degree; } + public getEdges() { return this.edges; } + public getData() { return this.data; } + + public updateData(data: any) { + this.data = data; + } + + public addEdge(edge: Edge) { + this.edges.push(edge); + this.degree++; + } + + public removeEdge(edge: Edge) { + const index = this.edges.indexOf(edge); + if (index == -1) throw new Error(`Unable to identify edge ${edge.getId()} from vertex ${this.id}.`); + this.edges.splice(index, 1); + this.degree--; + } + +} \ No newline at end of file diff --git a/src/services/reputation/reputation.service.ts b/src/services/reputation/reputation.service.ts new file mode 100644 index 0000000..4f33503 --- /dev/null +++ b/src/services/reputation/reputation.service.ts @@ -0,0 +1,4 @@ + +export class ReputationService { + +} \ No newline at end of file diff --git a/src/services/reputation/reputation/citation.ts b/src/services/reputation/reputation/citation.ts new file mode 100644 index 0000000..1d85e8e --- /dev/null +++ b/src/services/reputation/reputation/citation.ts @@ -0,0 +1,23 @@ +import { Post } from "./post"; + +export class Citation { + + private sourcePost: Post; + private citedPost: Post; + private impact: number + + constructor(source: Post, cited:Post, impact: number) { + this.sourcePost = source; + this.citedPost = cited; + this.impact = impact; + } + + public getSourcePost() { return this.sourcePost; } + public getCitedPost() { return this.citedPost; } + public getImpact() { return this.impact; } + + public isNeutral() { return this.impact == 0; } + public isNegative() { return this.impact < 0; } + public isPositive() { return this.impact > 0; } + +} \ No newline at end of file diff --git a/src/services/reputation/reputation/member.ts b/src/services/reputation/reputation/member.ts new file mode 100644 index 0000000..7642a7b --- /dev/null +++ b/src/services/reputation/reputation/member.ts @@ -0,0 +1,65 @@ +import { randomUUID } from "crypto"; + +export interface LedgerEntry { + timestamp: number, + type: ('post' | 'citation'), + postId: string, + citationId: string | null, + change: number, + balance: number +} + +export class Member { + + static localStore: Map; + + private id: string; + private reputation: number; + private ledger: LedgerEntry[]; + + constructor(id?: string) { + this.id = id ? id : randomUUID(); + this.reputation = 0; + this.ledger = []; + Member.localStore.set(this.id, this); + } + + public static getAllMembers() { + return Array.from(Member.localStore.values()); + } + + public static getMember(id: string) { + return Member.localStore.get(id); + } + + public getId() { return this.id; } + public getReputation() { return this.reputation; } + public getLedger() { return this.ledger; } + + public postReputation(postId: string, amount: number) { + const entry: LedgerEntry = { + timestamp: Date.now(), + type: "post", + postId: postId, + citationId: null, + change: amount, + balance: this.reputation + amount + } + this.reputation = entry.balance; + return entry; + } + + public citationReputation(postId: string, citationId: string, amount: number) { + const entry: LedgerEntry = { + timestamp: Date.now(), + type: "citation", + postId: postId, + citationId: citationId, + change: amount, + balance: this.reputation + amount + } + this.reputation = entry.balance; + return entry; + } + +} \ No newline at end of file diff --git a/src/services/reputation/reputation/post.ts b/src/services/reputation/reputation/post.ts new file mode 100644 index 0000000..9c8bc2c --- /dev/null +++ b/src/services/reputation/reputation/post.ts @@ -0,0 +1,112 @@ +import { Citation } from "./citation"; +import { Member } from "./member"; + +export interface AuthorWeight { + weight: number; + author: Member +} + +export interface CitationWeight { + weight: number; + citation: Citation; +} + +export class Post { + + private static DEFAULT_WEIGHT = 1000; + + private title: string; + private content: string; + private authors: AuthorWeight[]; + private citations: CitationWeight[]; + private authorsWeightTotal: number; + private citationsWeightTotal: number; + + constructor (title: string, content: string, authors: Member | Member[]) { + this.title = title; + this.content = content; + this.authors = []; + this.citations = []; + this.authorsWeightTotal = 0; + this.citationsWeightTotal = 0; + if (!Array.isArray(authors)) authors = [authors]; + for (const author of authors) { + this.authorsWeightTotal += Post.DEFAULT_WEIGHT; + this.authors.push({ + weight: Post.DEFAULT_WEIGHT, + author: author + }); + } + } + + public static serialize(post: Post, pretty: boolean = true) { + //TODO: Need to convert citations and authors to IDs. + if (!pretty) return JSON.stringify(Post); + return JSON.stringify(Post, undefined, 2); + } + + public static parse(serializedPost: string) { + //TODO: Need to retrieve citations and authors from IDs. + try { + const parsed = JSON.parse(serializedPost); + const title = parsed?.title; + const content = parsed.content; + const authors = parsed.authors; + const citations = parsed.citations; + const post = new Post(title, content, authors[0].author); + for (const author of authors) post.setAuthor(author.author, author.weight); + for (const citation of citations) post.setCitation(citation.citation, citation.weight); + return post; + } catch(err) { + throw new Error("Failed to parse the serialized post. \n - " + err.message); + } + } + + public getTitle() { return this.title; } + public getContent() { return this.content; } + public getAuthors() { return this.authors.map(({author}) => author); } + public getCitations() { return this.citations.map(({citation}) => citation); } + public getAuthorsWeightTotal() { return this.authorsWeightTotal; } + public getCitationWeightTotal() { return this.citationsWeightTotal; } + + public setAuthor(author: Member, weight: number = Post.DEFAULT_WEIGHT) { + this.removeAuthor(author); + this.authors.push({author, weight}); + this.authorsWeightTotal += weight; + } + + public removeAuthor(author: Member) { + const index = this.getAuthors().indexOf(author); + if (index != -1) { + const current = this.authors[index]; + this.authorsWeightTotal -= current.weight; + this.authors.splice(index, 1); + } + } + + public setCitation(citation: Citation, weight: number = Post.DEFAULT_WEIGHT) { + this.removeCitation(citation); + this.citations.push({citation, weight}); + this.citationsWeightTotal += weight; + } + + + public removeCitation(citation: Citation) { + const index = this.getCitations().indexOf(citation); + if (index != -1) { + const current = this.citations[index]; + this.citationsWeightTotal -= current.weight; + this.citations.splice(index, 1); + } + } + + public getWeight(weighted: Member | Citation) { + const forAuthor = weighted instanceof Member; + const arr = forAuthor ? this.authors : this.citations; + const key = forAuthor ? "author" : "citation"; + const index = arr.findIndex(item => item[key] == weighted); + if (index == -1) return -1; + return arr[index].weight; + } + +} \ No newline at end of file