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"
2024-03-28 02:23:13 +01:00
import ScriptUtils from "../../../scripts/ScriptUtils"
2020-12-08 23:44:34 +01:00
export class MangroveIdentity {
2024-04-13 02:40:21 +02:00
private readonly keypair : UIEventSource < CryptoKeyPair > = new UIEventSource < CryptoKeyPair > (
undefined
)
2024-02-20 16:53:26 +01:00
/ * *
* Same as the one in the user settings
* /
public readonly mangroveIdentity : UIEventSource < string >
2024-04-02 13:32:19 +02:00
private readonly key_id : UIEventSource < string > = new UIEventSource < string > ( undefined )
2024-02-20 16:53:26 +01:00
private readonly _mangroveIdentityCreationDate : UIEventSource < string >
2020-12-08 23:44:34 +01:00
2024-04-13 02:40:21 +02: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-01-21 23:58:14 +01:00
mangroveIdentity . addCallbackAndRunD ( async ( data ) = > {
2024-04-13 02:40:21 +02:00
await this . setKeypair ( data )
2023-09-28 23:50:27 +02:00
} )
2020-12-08 23:44:34 +01:00
}
2024-04-13 02:40:21 +02:00
private async setKeypair ( data : string ) {
console . log ( "Setting keypair from" , data )
2024-04-02 13:32:19 +02:00
const keypair = await MangroveReviews . jwkToKeypair ( JSON . parse ( data ) )
this . keypair . setData ( keypair )
const pem = await MangroveReviews . publicToPem ( keypair . publicKey )
this . 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-04-02 13:32:19 +02:00
this . keypair . setData ( keypair )
const pem = await MangroveReviews . publicToPem ( keypair . publicKey )
this . key_id . setData ( pem )
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-04-02 13:32:19 +02:00
if ( this . keypair . data === undefined ) {
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-04-13 02:40:21 +02:00
private geoReviewsById : Store < ( Review & { kid : string ; signature : string } ) [ ] > = undefined
2024-03-28 02:23:13 +01:00
2024-04-13 02:40:21 +02:00
public getGeoReviews ( ) : Store < ( Review & { kid : string ; signature : string } ) [ ] | undefined > {
2024-03-28 02:23:13 +01:00
if ( ! this . geoReviewsById ) {
const all = this . getAllReviews ( )
2024-04-13 02:40:21 +02:00
this . geoReviewsById = this . getAllReviews ( ) . mapD ( ( reviews ) = >
reviews . filter ( ( review ) = > {
2024-04-02 13:32:19 +02:00
try {
const subjectUrl = new URL ( review . sub )
return subjectUrl . protocol === "geo:"
} catch ( e ) {
2024-03-28 02:23:13 +01:00
return false
}
2024-04-13 02:40:21 +02:00
} )
)
2024-03-28 02:23:13 +01:00
}
return this . geoReviewsById
}
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-03-28 02:23:13 +01:00
* The returned store will contain ` undefined ` if still loading
2024-02-15 03:11:10 +01:00
* /
2024-03-28 02:23:13 +01:00
public getAllReviews ( ) : Store < ( Review & { kid : string ; signature : string } ) [ ] | undefined > {
2024-02-20 13:33:38 +01:00
if ( this . allReviewsById !== undefined ) {
2024-02-15 03:11:10 +01:00
return this . allReviewsById
}
2024-03-28 02:23:13 +01:00
this . allReviewsById = new UIEventSource ( undefined )
this . key_id . map ( async ( pem ) = > {
2024-02-20 13:33:38 +01:00
if ( pem === undefined ) {
2024-02-15 03:11:10 +01:00
return [ ]
}
2024-03-28 02:23:13 +01:00
const allReviews = await MangroveReviews . getReviews ( {
2024-04-13 02:40:21 +02:00
kid : pem ,
2024-02-15 03:11:10 +01:00
} )
2024-03-28 02:23:13 +01:00
this . allReviewsById . setData (
allReviews . reviews . map ( ( r ) = > ( {
. . . r ,
2024-04-13 02:40:21 +02:00
. . . r . payload ,
2024-03-28 02:23:13 +01:00
} ) )
)
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
2024-04-02 13:32:19 +02:00
private readonly _testmode : Store < boolean >
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 > > ,
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
2024-04-02 13:32:19 +02:00
} ,
testmode? : Store < boolean >
2022-09-08 21:40:48 +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
2024-04-02 13:32:19 +02:00
this . _testmode = testmode ? ? new ImmutableStore ( false )
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 > > ,
2024-04-02 13:32:19 +02:00
mangroveIdentity : MangroveIdentity ,
options : {
2023-01-21 23:58:14 +01:00
nameKey ? : "name" | string
fallbackName? : string
uncertaintyRadius? : number
2024-04-02 13:32:19 +02:00
} ,
testmode : Store < boolean >
) : FeatureReviews {
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
}
2024-04-13 02:40:21 +02:00
const featureReviews = new FeatureReviews (
feature ,
tagsSource ,
mangroveIdentity ,
options ,
testmode
)
2023-09-28 23:50:27 +02: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 > {
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 ,
2024-04-13 02:40:21 +02:00
. . . review ,
2023-09-28 23:50:27 +02:00
}
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 )
2024-04-02 13:32:19 +02:00
if ( ! this . _testmode . data ) {
await MangroveReviews . submitReview ( jwt )
} else {
console . log ( "Testmode enabled - not uploading review" )
await Utils . waitFor ( 1000 )
}
2024-02-15 03:11:10 +01:00
const reviewWithKid = {
2023-12-24 05:01:10 +01:00
. . . r ,
kid ,
signature : jwt ,
2024-04-13 02:40:21 +02:00
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
2024-04-13 02:40:21 +02:00
} ) ,
2023-09-28 23:50:27 +02:00
} )
hasNew = true
2022-09-08 21:40:48 +02: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
2024-04-13 02:40:21 +02:00
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
}
2022-09-08 21:40:48 +02:00
}