2023-09-28 23:50:27 +02:00
import { ImmutableStore , Store , UIEventSource } from "../UIEventSource"
import { MangroveReviews , Review } from "mangrove-reviews-typescript"
import { Utils } from "../../Utils"
import { Feature , Position } from "geojson"
import { GeoOperations } from "../GeoOperations"
2020-12-08 23:44:34 +01:00
export class MangroveIdentity {
2024-02-15 03:11:10 +01:00
private readonly keypair : Store < CryptoKeyPair >
2024-02-20 16:53:26 +01:00
/ * *
* Same as the one in the user settings
* /
public readonly mangroveIdentity : UIEventSource < string >
2024-02-15 03:11:10 +01:00
private readonly key_id : Store < string >
2024-02-20 16:53:26 +01:00
private readonly _mangroveIdentityCreationDate : UIEventSource < string >
2020-12-08 23:44:34 +01:00
2024-02-20 16:53:26 +01:00
constructor ( mangroveIdentity : UIEventSource < string > , mangroveIdentityCreationDate : UIEventSource < string > ) {
2024-02-15 03:11:10 +01:00
this . mangroveIdentity = mangroveIdentity
2024-02-20 16:53:26 +01:00
this . _mangroveIdentityCreationDate = mangroveIdentityCreationDate
2023-09-28 23:50:27 +02:00
const key_id = new UIEventSource < string > ( undefined )
this . key_id = key_id
const keypairEventSource = new UIEventSource < CryptoKeyPair > ( undefined )
this . keypair = keypairEventSource
2023-01-21 23:58:14 +01:00
mangroveIdentity . addCallbackAndRunD ( async ( data ) = > {
2023-11-23 17:06:30 +01:00
if ( ! data ) {
2023-09-28 23:50:27 +02:00
return
2020-12-08 23:44:34 +01:00
}
2023-09-28 23:50:27 +02: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
}
/ * *
* Creates an identity if none exists already .
* Is written into the UIEventsource , which was passed into the constructor
* @constructor
* /
2024-02-20 16:53:26 +01:00
private async CreateIdentity ( ) : Promise < void > {
2023-09-28 23:50:27 +02:00
const keypair = await MangroveReviews . generateKeypair ( )
const jwk = await MangroveReviews . keypairToJwk ( keypair )
2024-02-20 16:53:26 +01:00
if ( ( this . mangroveIdentity . data ? ? "" ) !== "" ) {
2023-01-21 23:58:14 +01:00
// Identity has been loaded via osmPreferences by now - we don't overwrite
2023-09-28 23:50:27 +02:00
return
2020-12-08 23:44:34 +01:00
}
2024-02-15 03:11:10 +01:00
console . log ( "Creating a new Mangrove identity!" )
2024-02-20 16:53:26 +01:00
this . mangroveIdentity . setData ( JSON . stringify ( jwk ) )
this . _mangroveIdentityCreationDate . setData ( new Date ( ) . toISOString ( ) )
2020-12-08 23:44:34 +01:00
}
2024-02-15 03:11:10 +01:00
/ * *
* Only called to create a review .
* /
async getKeypair ( ) : Promise < CryptoKeyPair > {
2024-02-20 13:33:38 +01:00
if ( this . keypair . data ? ? "" === "" ) {
2024-02-15 03:11:10 +01:00
// We want to create a review, but it seems like no key has been setup at this moment
// We create the key
try {
if ( ! Utils . runningFromConsole && ( this . mangroveIdentity . data ? ? "" ) === "" ) {
2024-02-20 16:53:26 +01:00
await this . CreateIdentity ( )
2024-02-15 03:11:10 +01:00
}
} catch ( e ) {
console . error ( "Could not create identity: " , e )
}
}
return this . keypair . data
}
getKeyId ( ) : Store < string > {
return this . key_id
}
2024-02-20 13:33:38 +01:00
private allReviewsById : UIEventSource < ( Review & { kid : string ; signature : string } ) [ ] > =
undefined
2024-02-15 03:11:10 +01:00
/ * *
* Gets all reviews that are made for the current identity .
* /
2024-02-20 13:33:38 +01:00
public getAllReviews ( ) : Store < ( Review & { kid : string ; signature : string } ) [ ] > {
if ( this . allReviewsById !== undefined ) {
2024-02-15 03:11:10 +01:00
return this . allReviewsById
}
2024-02-20 13:33:38 +01:00
this . allReviewsById = new UIEventSource ( [ ] )
this . key_id . map ( ( pem ) = > {
if ( pem === undefined ) {
2024-02-15 03:11:10 +01:00
return [ ]
}
MangroveReviews . getReviews ( {
2024-02-20 13:33:38 +01:00
kid : pem ,
} ) . then ( ( allReviews ) = > {
this . allReviewsById . setData (
allReviews . reviews . map ( ( r ) = > ( {
. . . r ,
. . . r . payload ,
} ) )
)
2024-02-15 03:11:10 +01:00
} )
} )
return this . allReviewsById
}
2024-02-20 13:33:38 +01:00
addReview ( review : Review & { kid ; signature } ) {
2024-02-15 03:11:10 +01:00
this . allReviewsById ? . setData ( this . allReviewsById ? . data ? . concat ( [ review ] ) )
}
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 {
2024-02-12 15:53:37 +01:00
/ * *
* See https : //gitlab.com/open-reviews/mangrove/-/blob/master/servers/reviewer/src/review.rs#L269 and https://github.com/pietervdvn/MapComplete/issues/1775
* /
public static readonly REVIEW_OPINION_MAX_LENGTH = 1000
2023-09-28 23:50:27 +02:00
private static readonly _featureReviewsCache : Record < string , FeatureReviews > = { }
public readonly subjectUri : Store < string >
public readonly average : Store < number | null >
2023-12-24 05:01:10 +01:00
private readonly _reviews : UIEventSource <
( Review & { kid : string ; signature : string ; madeByLoggedInUser : Store < boolean > } ) [ ]
> = new UIEventSource ( [ ] )
2023-01-21 23:58:14 +01:00
public readonly reviews : Store < ( Review & { madeByLoggedInUser : Store < boolean > } ) [ ] > =
2023-09-28 23:50:27 +02:00
this . _reviews
private readonly _lat : number
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
2021-04-23 17:22:01 +02:00
private constructor (
2023-03-28 05:13:48 +02:00
feature : Feature ,
tagsSource : UIEventSource < Record < string , string > > ,
2024-02-20 16:53:26 +01:00
mangroveIdentity : MangroveIdentity ,
2023-01-21 23:58:14 +01:00
options ? : {
nameKey ? : "name" | string
fallbackName? : string
uncertaintyRadius? : number
}
2021-04-23 17:22:01 +02:00
) {
2023-01-21 23:58:14 +01:00
const centerLonLat = GeoOperations . centerpointCoordinates ( feature )
2023-09-28 23:50:27 +02:00
; [ this . _lon , this . _lat ] = centerLonLat
2024-02-20 16:53:26 +01:00
this . _identity = mangroveIdentity
2023-09-28 23:50:27 +02:00
const nameKey = options ? . nameKey ? ? "name"
2023-01-21 23:58:14 +01:00
if ( feature . geometry . type === "Point" ) {
2023-09-28 23:50:27 +02:00
this . _uncertainty = options ? . uncertaintyRadius ? ? 10
2023-01-21 23:58:14 +01:00
} else {
2023-09-28 23:50:27 +02:00
let coordss : Position [ ] [ ]
2023-01-21 23:58:14 +01:00
if ( feature . geometry . type === "LineString" ) {
2023-09-28 23:50:27 +02:00
coordss = [ feature . geometry . coordinates ]
2023-01-21 23:58:14 +01:00
} else if (
feature . geometry . type === "MultiLineString" ||
feature . geometry . type === "Polygon"
) {
2023-09-28 23:50:27 +02:00
coordss = feature . geometry . coordinates
2023-01-21 23:58:14 +01:00
}
2023-09-28 23:50:27 +02: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-09-28 23:50:27 +02:00
)
2023-02-06 00:30:50 +01:00
}
2023-01-21 23:58:14 +01:00
}
2023-09-28 23:50:27 +02:00
this . _uncertainty = options ? . uncertaintyRadius ? ? maxDistance
2021-04-23 17:22:01 +02:00
}
2023-09-28 23:50:27 +02:00
this . _name = tagsSource . map ( ( tags ) = > tags [ nameKey ] ? ? options ? . fallbackName )
2023-01-21 23:58:14 +01:00
2023-09-28 23:50:27 +02:00
this . subjectUri = this . ConstructSubjectUri ( )
2023-01-21 23:58:14 +01:00
2023-09-28 23:50:27 +02:00
const self = this
2023-01-21 23:58:14 +01:00
this . subjectUri . addCallbackAndRunD ( async ( sub ) = > {
2023-09-28 23:50:27 +02:00
const reviews = await MangroveReviews . getReviews ( { sub } )
self . addReviews ( reviews . reviews )
} )
2023-01-21 23:58:14 +01:00
/ * W e a l s o c o n s t r u c t a l l s u b j e c t q u e r i e s _ w i t h o u t _ e n c o d i n g t h e n a m e t o w o r k a r o u n d a p r e v i o u s b u g
* See https : //github.com/giggls/opencampsitemap/issues/30
* /
this . ConstructSubjectUri ( true ) . addCallbackAndRunD ( async ( sub ) = > {
try {
2023-09-28 23:50:27 +02:00
const reviews = await MangroveReviews . getReviews ( { sub } )
self . addReviews ( reviews . reviews )
2023-01-21 23:58:14 +01:00
} catch ( e ) {
2023-09-28 23:50:27 +02:00
console . log ( "Could not fetch reviews for partially incorrect query " , sub )
2023-01-21 23:58:14 +01:00
}
2023-09-28 23:50:27 +02:00
} )
this . average = this . _reviews . map ( ( reviews ) = > {
2023-09-28 04:02:42 +02:00
if ( ! reviews ) {
2023-09-28 23:50:27 +02:00
return null
2023-09-28 04:02:42 +02:00
}
2023-09-28 23:50:27 +02:00
if ( reviews . length === 0 ) {
2023-09-28 04:02:42 +02:00
return null
}
2023-09-28 23:50:27 +02:00
let sum = 0
let count = 0
2023-09-28 04:02:42 +02:00
for ( const review of reviews ) {
if ( review . rating !== undefined ) {
2023-09-28 23:50:27 +02:00
count ++
sum += review . rating
2023-09-28 04:02:42 +02:00
}
}
return Math . round ( sum / count )
2023-09-28 23:50:27 +02:00
} )
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
}
2021-04-23 17:22:01 +02:00
) {
2023-09-28 23:50:27 +02:00
const key = feature . properties . id
const cached = FeatureReviews . _featureReviewsCache [ key ]
2021-04-23 17:22:01 +02:00
if ( cached !== undefined ) {
2023-09-28 23:50:27 +02:00
return cached
2020-12-08 23:44:34 +01:00
}
2023-09-28 23:50:27 +02:00
const featureReviews = new FeatureReviews ( feature , tagsSource , mangroveIdentity , options )
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 > {
2024-02-20 13:33:38 +01:00
if (
review . opinion !== undefined &&
review . opinion . length > FeatureReviews . REVIEW_OPINION_MAX_LENGTH
) {
throw (
"Opinion too long, should be at most " +
FeatureReviews . REVIEW_OPINION_MAX_LENGTH +
" characters long"
)
2024-02-12 15:53:37 +01:00
}
2023-01-21 23:58:14 +01:00
const r : Review = {
sub : this.subjectUri.data ,
2023-09-28 23:50:27 +02:00
. . . review ,
}
2024-02-15 03:11:10 +01:00
const keypair : CryptoKeyPair = await this . _identity . getKeypair ( )
2023-09-28 23:50:27 +02:00
const jwt = await MangroveReviews . signReview ( keypair , r )
2023-12-24 05:01:10 +01:00
const kid = await MangroveReviews . publicToPem ( keypair . publicKey )
2023-09-28 23:50:27 +02:00
await MangroveReviews . submitReview ( jwt )
2024-02-15 03:11:10 +01:00
const reviewWithKid = {
2023-12-24 05:01:10 +01:00
. . . r ,
kid ,
signature : jwt ,
madeByLoggedInUser : new ImmutableStore ( true ) ,
2024-02-15 03:11:10 +01:00
}
2024-02-20 13:33:38 +01:00
this . _reviews . data . push ( reviewWithKid )
2023-09-28 23:50:27 +02:00
this . _reviews . ping ( )
2024-02-15 03:11:10 +01:00
this . _identity . addReview ( reviewWithKid )
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-12-24 05:01:10 +01:00
private addReviews ( reviews : { payload : Review ; kid : string ; signature : string } [ ] ) {
2023-09-28 23:50:27 +02:00
const self = this
const alreadyKnown = new Set ( self . _reviews . data . map ( ( r ) = > r . rating + " " + r . opinion ) )
2023-01-21 23:58:14 +01:00
2023-09-28 23:50:27 +02:00
let hasNew = false
2023-01-21 23:58:14 +01:00
for ( const reviewData of reviews ) {
2023-09-28 23:50:27 +02:00
const review = reviewData . payload
2023-01-21 23:58:14 +01:00
try {
2023-09-28 23:50:27 +02:00
const url = new URL ( review . sub )
2023-01-21 23:58:14 +01:00
if ( url . protocol === "geo:" ) {
const coordinate = < [ number , number ] > (
url . pathname . split ( "," ) . map ( ( n ) = > Number ( n ) )
2023-09-28 23:50:27 +02:00
)
2023-01-21 23:58:14 +01:00
const distance = GeoOperations . distanceBetween (
[ this . _lat , this . _lon ] ,
coordinate
2023-09-28 23:50:27 +02:00
)
2023-01-21 23:58:14 +01:00
if ( distance > this . _uncertainty ) {
2023-09-28 23:50:27 +02:00
continue
2023-01-17 01:00:43 +01:00
}
}
2023-01-21 23:58:14 +01:00
} catch ( e ) {
2023-09-28 23:50:27 +02:00
console . warn ( e )
2023-01-21 23:58:14 +01:00
}
2020-12-08 23:44:34 +01:00
2023-09-28 23:50:27 +02:00
const key = review . rating + " " + review . opinion
2023-01-21 23:58:14 +01:00
if ( alreadyKnown . has ( key ) ) {
2023-09-28 23:50:27 +02:00
continue
2023-01-21 23:58:14 +01:00
}
self . _reviews . data . push ( {
. . . review ,
2023-12-24 05:01:10 +01:00
kid : reviewData.kid ,
signature : reviewData.signature ,
2024-02-15 03:11:10 +01:00
madeByLoggedInUser : this._identity.getKeyId ( ) . map ( ( user_key_id ) = > {
2023-09-28 23:50:27 +02:00
return reviewData . kid === user_key_id
} ) ,
} )
hasNew = true
2020-12-08 23:44:34 +01:00
}
2023-01-21 23:58:14 +01:00
if ( hasNew ) {
2023-09-28 04:02:42 +02:00
self . _reviews . data . sort ( ( a , b ) = > b . iat - a . iat ) // Sort with most recent first
2023-09-28 23:50:27 +02:00
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
2023-09-28 23:50:27 +02:00
const self = this
return this . _name . map ( function ( name ) {
2024-01-01 20:20:21 +01:00
let uri = ` geo: ${ self . _lat } , ${ self . _lon } ?u= ${ Math . round ( self . _uncertainty ) } `
2023-01-21 23:58:14 +01:00
if ( name ) {
2023-09-28 23:50:27 +02:00
uri += "&q=" + ( dontEncodeName ? name : encodeURIComponent ( name ) )
2020-12-11 15:27:52 +01:00
}
2023-09-28 23:50:27 +02:00
return uri
} )
2020-12-07 03:02:50 +01:00
}
}