| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  | import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" | 
					
						
							|  |  |  | import { MangroveReviews, Review } from "mangrove-reviews-typescript" | 
					
						
							|  |  |  | import { Utils } from "../../Utils" | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  | import { Feature, Position } from "geojson" | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  | import { GeoOperations } from "../GeoOperations" | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | export class MangroveIdentity { | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |     public readonly keypair: Store<CryptoKeyPair> | 
					
						
							|  |  |  |     public readonly key_id: Store<string> | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     constructor(mangroveIdentity: UIEventSource<string>) { | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |         const key_id = new UIEventSource<string>(undefined) | 
					
						
							|  |  |  |         this.key_id = key_id | 
					
						
							|  |  |  |         const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined) | 
					
						
							|  |  |  |         this.keypair = keypairEventSource | 
					
						
							|  |  |  |         mangroveIdentity.addCallbackAndRunD(async (data) => { | 
					
						
							|  |  |  |             if (data === "") { | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  |                 return | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |             const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data)) | 
					
						
							|  |  |  |             keypairEventSource.setData(keypair) | 
					
						
							|  |  |  |             const pem = await MangroveReviews.publicToPem(keypair.publicKey) | 
					
						
							|  |  |  |             key_id.setData(pem) | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  |         }) | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-07 04:50:12 +01:00
										 |  |  |         try { | 
					
						
							| 
									
										
										
										
											2021-12-30 20:41:45 +01:00
										 |  |  |             if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") { | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |                 MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {}) | 
					
						
							| 
									
										
										
										
											2021-01-07 04:50:12 +01:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2021-04-23 17:22:01 +02:00
										 |  |  |         } catch (e) { | 
					
						
							| 
									
										
										
										
											2021-01-07 04:50:12 +01:00
										 |  |  |             console.error("Could not create identity: ", e) | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Creates an identity if none exists already. | 
					
						
							|  |  |  |      * Is written into the UIEventsource, which was passed into the constructor | 
					
						
							|  |  |  |      * @constructor | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |     private static async CreateIdentity(identity: UIEventSource<string>): Promise<void> { | 
					
						
							|  |  |  |         const keypair = await MangroveReviews.generateKeypair() | 
					
						
							|  |  |  |         const jwk = await MangroveReviews.keypairToJwk(keypair) | 
					
						
							|  |  |  |         if ((identity.data ?? "") !== "") { | 
					
						
							|  |  |  |             // Identity has been loaded via osmPreferences by now - we don't overwrite
 | 
					
						
							|  |  |  |             return | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |         identity.setData(JSON.stringify(jwk)) | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2020-12-07 03:02:50 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Tracks all reviews of a given feature, allows to create a new review | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export default class FeatureReviews { | 
					
						
							|  |  |  |     private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {} | 
					
						
							|  |  |  |     public readonly subjectUri: Store<string> | 
					
						
							|  |  |  |     private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store<boolean> })[]> = | 
					
						
							|  |  |  |         new UIEventSource([]) | 
					
						
							|  |  |  |     public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> = | 
					
						
							|  |  |  |         this._reviews | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  |     private readonly _lat: number | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |     private readonly _lon: number | 
					
						
							|  |  |  |     private readonly _uncertainty: number | 
					
						
							|  |  |  |     private readonly _name: Store<string> | 
					
						
							|  |  |  |     private readonly _identity: MangroveIdentity | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     private constructor( | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  |         feature: Feature, | 
					
						
							|  |  |  |         tagsSource: UIEventSource<Record<string, string>>, | 
					
						
							|  |  |  |         mangroveIdentity?: MangroveIdentity, | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |         options?: { | 
					
						
							|  |  |  |             nameKey?: "name" | string | 
					
						
							|  |  |  |             fallbackName?: string | 
					
						
							|  |  |  |             uncertaintyRadius?: number | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  |     ) { | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |         const centerLonLat = GeoOperations.centerpointCoordinates(feature) | 
					
						
							|  |  |  |         ;[this._lon, this._lat] = centerLonLat | 
					
						
							| 
									
										
										
										
											2023-06-14 20:39:36 +02:00
										 |  |  |         this._identity = | 
					
						
							|  |  |  |             mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined)) | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |         const nameKey = options?.nameKey ?? "name" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (feature.geometry.type === "Point") { | 
					
						
							|  |  |  |             this._uncertainty = options?.uncertaintyRadius ?? 10 | 
					
						
							|  |  |  |         } else { | 
					
						
							| 
									
										
										
										
											2023-02-06 00:30:50 +01:00
										 |  |  |             let coordss: Position[][] | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |             if (feature.geometry.type === "LineString") { | 
					
						
							| 
									
										
										
										
											2023-02-06 00:30:50 +01:00
										 |  |  |                 coordss = [feature.geometry.coordinates] | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |             } else if ( | 
					
						
							|  |  |  |                 feature.geometry.type === "MultiLineString" || | 
					
						
							|  |  |  |                 feature.geometry.type === "Polygon" | 
					
						
							|  |  |  |             ) { | 
					
						
							| 
									
										
										
										
											2023-02-06 00:30:50 +01:00
										 |  |  |                 coordss = feature.geometry.coordinates | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |             } | 
					
						
							|  |  |  |             let maxDistance = 0 | 
					
						
							| 
									
										
										
										
											2023-02-06 00:30:50 +01:00
										 |  |  |             for (const coords of coordss) { | 
					
						
							|  |  |  |                 for (const coord of coords) { | 
					
						
							|  |  |  |                     maxDistance = Math.max( | 
					
						
							|  |  |  |                         maxDistance, | 
					
						
							|  |  |  |                         GeoOperations.distanceBetween(centerLonLat, coord) | 
					
						
							|  |  |  |                     ) | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             this._uncertainty = options?.uncertaintyRadius ?? maxDistance | 
					
						
							| 
									
										
										
										
											2021-04-23 17:22:01 +02:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-06-14 20:39:36 +02:00
										 |  |  |         this._name = tagsSource.map((tags) => tags[nameKey] ?? options?.fallbackName) | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         this.subjectUri = this.ConstructSubjectUri() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const self = this | 
					
						
							|  |  |  |         this.subjectUri.addCallbackAndRunD(async (sub) => { | 
					
						
							|  |  |  |             const reviews = await MangroveReviews.getReviews({ sub }) | 
					
						
							|  |  |  |             self.addReviews(reviews.reviews) | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         /* We also construct all subject queries _without_ encoding the name to work around a previous bug | 
					
						
							|  |  |  |          * See https://github.com/giggls/opencampsitemap/issues/30
 | 
					
						
							|  |  |  |          */ | 
					
						
							|  |  |  |         this.ConstructSubjectUri(true).addCallbackAndRunD(async (sub) => { | 
					
						
							|  |  |  |             try { | 
					
						
							|  |  |  |                 const reviews = await MangroveReviews.getReviews({ sub }) | 
					
						
							|  |  |  |                 self.addReviews(reviews.reviews) | 
					
						
							|  |  |  |             } catch (e) { | 
					
						
							|  |  |  |                 console.log("Could not fetch reviews for partially incorrect query ", sub) | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         }) | 
					
						
							| 
									
										
										
										
											2021-04-23 17:22:01 +02:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Construct a featureReviewsFor or fetches it from the cache | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public static construct( | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  |         feature: Feature, | 
					
						
							|  |  |  |         tagsSource: UIEventSource<Record<string, string>>, | 
					
						
							|  |  |  |         mangroveIdentity?: MangroveIdentity, | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |         options?: { | 
					
						
							|  |  |  |             nameKey?: "name" | string | 
					
						
							|  |  |  |             fallbackName?: string | 
					
						
							|  |  |  |             uncertaintyRadius?: number | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  |     ) { | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |         const key = feature.properties.id | 
					
						
							|  |  |  |         const cached = FeatureReviews._featureReviewsCache[key] | 
					
						
							| 
									
										
										
										
											2021-04-23 17:22:01 +02:00
										 |  |  |         if (cached !== undefined) { | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  |             return cached | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  |         const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options) | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |         FeatureReviews._featureReviewsCache[key] = featureReviews | 
					
						
							|  |  |  |         return featureReviews | 
					
						
							| 
									
										
										
										
											2020-12-07 03:02:50 +01:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |      * The given review is uploaded to mangrove.reviews and added to the list of known reviews | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |     public async createReview(review: Omit<Review, "sub">): Promise<void> { | 
					
						
							|  |  |  |         const r: Review = { | 
					
						
							|  |  |  |             sub: this.subjectUri.data, | 
					
						
							|  |  |  |             ...review, | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |         const keypair: CryptoKeyPair = this._identity.keypair.data | 
					
						
							|  |  |  |         console.log(r) | 
					
						
							|  |  |  |         const jwt = await MangroveReviews.signReview(keypair, r) | 
					
						
							|  |  |  |         console.log("Signed:", jwt) | 
					
						
							|  |  |  |         await MangroveReviews.submitReview(jwt) | 
					
						
							|  |  |  |         this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) }) | 
					
						
							|  |  |  |         this._reviews.ping() | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-07 03:02:50 +01:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |      * Adds given reviews to the 'reviews'-UI-eventsource | 
					
						
							|  |  |  |      * @param reviews | 
					
						
							|  |  |  |      * @private | 
					
						
							| 
									
										
										
										
											2020-12-07 03:02:50 +01:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |     private addReviews(reviews: { payload: Review; kid: string }[]) { | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  |         const self = this | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |         const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         let hasNew = false | 
					
						
							|  |  |  |         for (const reviewData of reviews) { | 
					
						
							|  |  |  |             const review = reviewData.payload | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             try { | 
					
						
							|  |  |  |                 const url = new URL(review.sub) | 
					
						
							|  |  |  |                 console.log("URL is", url) | 
					
						
							|  |  |  |                 if (url.protocol === "geo:") { | 
					
						
							|  |  |  |                     const coordinate = <[number, number]>( | 
					
						
							|  |  |  |                         url.pathname.split(",").map((n) => Number(n)) | 
					
						
							| 
									
										
										
										
											2023-01-17 01:00:43 +01:00
										 |  |  |                     ) | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |                     const distance = GeoOperations.distanceBetween( | 
					
						
							|  |  |  |                         [this._lat, this._lon], | 
					
						
							|  |  |  |                         coordinate | 
					
						
							| 
									
										
										
										
											2023-01-17 01:00:43 +01:00
										 |  |  |                     ) | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |                     if (distance > this._uncertainty) { | 
					
						
							|  |  |  |                         continue | 
					
						
							| 
									
										
										
										
											2023-01-17 01:00:43 +01:00
										 |  |  |                     } | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |             } catch (e) { | 
					
						
							|  |  |  |                 console.warn(e) | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2020-12-08 23:44:34 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |             const key = review.rating + " " + review.opinion | 
					
						
							|  |  |  |             if (alreadyKnown.has(key)) { | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             self._reviews.data.push({ | 
					
						
							|  |  |  |                 ...review, | 
					
						
							|  |  |  |                 madeByLoggedInUser: this._identity.key_id.map((user_key_id) => { | 
					
						
							|  |  |  |                     return reviewData.kid === user_key_id | 
					
						
							|  |  |  |                 }), | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  |             }) | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |             hasNew = true | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |         if (hasNew) { | 
					
						
							|  |  |  |             self._reviews.ping() | 
					
						
							| 
									
										
										
										
											2020-12-11 15:27:52 +01:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Gets an URI which represents the item in a mangrove-compatible way | 
					
						
							|  |  |  |      * | 
					
						
							|  |  |  |      * See https://mangrove.reviews/standard#mangrove-core-uri-schemes
 | 
					
						
							|  |  |  |      * @constructor | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     private ConstructSubjectUri(dontEncodeName: boolean = false): Store<string> { | 
					
						
							|  |  |  |         // https://www.rfc-editor.org/rfc/rfc5870#section-3.4.2
 | 
					
						
							|  |  |  |         // `u` stands for `uncertainty`, https://www.rfc-editor.org/rfc/rfc5870#section-3.4.3
 | 
					
						
							|  |  |  |         const self = this | 
					
						
							|  |  |  |         return this._name.map(function (name) { | 
					
						
							|  |  |  |             let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}` | 
					
						
							|  |  |  |             if (name) { | 
					
						
							|  |  |  |                 uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name)) | 
					
						
							| 
									
										
										
										
											2020-12-11 15:27:52 +01:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2023-01-21 23:58:14 +01:00
										 |  |  |             return uri | 
					
						
							|  |  |  |         }) | 
					
						
							| 
									
										
										
										
											2020-12-07 03:02:50 +01:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-09-08 21:40:48 +02:00
										 |  |  | } |