From d990dd0319f4ce9fdf065dbba7af2ac128f193c5 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Tue, 10 May 2022 22:24:57 +0200 Subject: [PATCH 01/43] Make backend tests pass --- deno-backend/auth.ts | 45 ++ deno-backend/database.ts | 148 +++++ deno-backend/events.ts | 1 + deno-backend/index.ts | 73 +++ deno-backend/instance-keys.ts | 2 + deno-backend/pubsub.ts | 381 +++++++++++++ deno-backend/router.ts | 363 ++++++++++++ deno-backend/routes.ts | 197 +++++++ deno-backend/server.ts | 108 ++++ deno-backend/types.ts | 36 ++ deno-shared/declarations.ts | 66 +++ deno-shared/domains/chelonia/GIMessage.ts | 120 ++++ deno-shared/domains/chelonia/chelonia.ts | 379 +++++++++++++ deno-shared/domains/chelonia/db.ts | 87 +++ deno-shared/domains/chelonia/errors.ts | 41 ++ deno-shared/domains/chelonia/events.ts | 3 + deno-shared/domains/chelonia/internals.ts | 332 +++++++++++ deno-shared/functions.ts | 45 ++ deno-shared/giLodash.ts | 214 +++++++ deno-shared/pubsub.test.ts | 52 ++ deno-shared/pubsub.ts | 660 ++++++++++++++++++++++ deno-shared/string.ts | 35 ++ import-map.json | 17 + shared/domains/chelonia/internals.js | 3 + shared/pubsub.js | 9 +- test/backend.test.js | 5 +- 26 files changed, 3417 insertions(+), 5 deletions(-) create mode 100644 deno-backend/auth.ts create mode 100644 deno-backend/database.ts create mode 100644 deno-backend/events.ts create mode 100644 deno-backend/index.ts create mode 100644 deno-backend/instance-keys.ts create mode 100644 deno-backend/pubsub.ts create mode 100644 deno-backend/router.ts create mode 100644 deno-backend/routes.ts create mode 100644 deno-backend/server.ts create mode 100644 deno-backend/types.ts create mode 100644 deno-shared/declarations.ts create mode 100644 deno-shared/domains/chelonia/GIMessage.ts create mode 100644 deno-shared/domains/chelonia/chelonia.ts create mode 100644 deno-shared/domains/chelonia/db.ts create mode 100644 deno-shared/domains/chelonia/errors.ts create mode 100644 deno-shared/domains/chelonia/events.ts create mode 100644 deno-shared/domains/chelonia/internals.ts create mode 100644 deno-shared/functions.ts create mode 100644 deno-shared/giLodash.ts create mode 100644 deno-shared/pubsub.test.ts create mode 100644 deno-shared/pubsub.ts create mode 100644 deno-shared/string.ts create mode 100644 import-map.json diff --git a/deno-backend/auth.ts b/deno-backend/auth.ts new file mode 100644 index 0000000000..a7ec563084 --- /dev/null +++ b/deno-backend/auth.ts @@ -0,0 +1,45 @@ +// create our auth plugin. see: +// https://hapijs.com/tutorials/auth +// https://hapijs.com/tutorials/plugins + +const { AlreadyExists, BadRequest, NotFound, PermissionDenied } = Deno.errors + +import { verify, b64ToStr } from '~/shared/functions.ts' + +export default { + name: 'gi-auth', + register (server: Object, opts: Object) { + server.auth.scheme('gi-auth', function (server, options) { + return { + authenticate (request, h) { + const { authorization } = request.headers + if (!authorization) h.unauthenticated(new PermissionDenied('Missing authorization')) + + let [scheme, json] = authorization.split(/\s+/) + // NOTE: if you want to add any signature verification, do it here + // eslint-disable-next-line no-constant-condition + if (false) { + if (!scheme.includes('gi')) h.unauthenticated(new BadRequest('Bad authentication')) + + try { + json = JSON.parse(b64ToStr(json)) + } catch (e) { + return h.unauthenticated(new BadRequest('Invalid token format')) + } + // http://hapijs.com/api/#serverauthschemename-scheme + const isValid = verify(json.msg, json.key, json.sig) + json.userId = json.key + const credentials = { credentials: json } + if (!isValid) return h.unauthenticated(new PermissionDenied('Bad credentials'), credentials) + return h.authenticated(credentials) + } else { + // remove this if you decide to implement it + return h.authenticated({ credentials: 'TODO: delete me' }) + } + } + } + }) + + server.auth.strategy('gi-auth', 'gi-auth') + } +} diff --git a/deno-backend/database.ts b/deno-backend/database.ts new file mode 100644 index 0000000000..1ea7221a6d --- /dev/null +++ b/deno-backend/database.ts @@ -0,0 +1,148 @@ +import * as pathlib from 'path' + +import sbp from "@sbp/sbp" + +import '~/shared/domains/chelonia/db.ts' +import { strToB64 } from '~/shared/functions.ts' + +const CI = Deno.env.get('CI') +const GI_VERSION = Deno.env.get('GI_VERSION') +const NODE_ENV = Deno.env.get('NODE_ENV') + +const { AlreadyExists, BadRequest, NotFound } = Deno.errors +const dataFolder = pathlib.resolve('./data') +const production = NODE_ENV === 'production' +const readFileAsync = Deno.readFile +const readTextFileAsync = Deno.readTextFile +const writeFileAsync = Deno.writeFile +const writeTextFileAsync = Deno.writeTextFile + +const dirExists = async (pathname) => { + try { + const stats = await Deno.stat(pathname) + return stats ?? stats.isDirectory() + } catch { + return false + } +} + +const fileExists = async (pathname) => { + try { + const stats = await Deno.stat(pathname) + return stats ?? stats.isFile() + } catch { + return false + } +} + +if (!(await dirExists(dataFolder))) { + await Deno.mkdir(dataFolder, { mode: 0o750, recursive: true }) +} + +export default (sbp('sbp/selectors/register', { + 'backend/db/streamEntriesSince': async function (contractID: string, hash: string) { + let currentHEAD = await sbp('chelonia/db/latestHash', contractID) + if (!currentHEAD) { + throw new NotFound(`contractID ${contractID} doesn't exist!`) + } + const chunks = ['['] + try { + while (true) { + const entry = await sbp('chelonia/db/getEntry', currentHEAD) + if (!entry) { + console.error(`read(): entry ${currentHEAD} no longer exists.`) + chunks[chunks.length] = ']' + break + } + if (chunks.length > 1) chunks[chunks.length] = ',' + chunks[chunks.length] = `"${strToB64(entry.serialize())}"` + if (currentHEAD === hash) { + chunks[chunks.length] = ']' + break + } else { + currentHEAD = entry.message().previousHEAD + } + } + } catch (error) { + console.error(`read(): ${error.message}:`, error) + } + const json = chunks.join('') + // console.log('streamEntriesSince text:', json) + return json + }, + // ======================= + // wrapper methods to add / lookup names + // ======================= + 'backend/db/registerName': async function (name: string, value: string) { + const lookup = await sbp('backend/db/lookupName', name) || null + if (lookup) { + if (!(lookup instanceof Error)) { + return new AlreadyExists(`in backend/db/registerName: ${name}`) + } + if (!(lookup instanceof NotFound)) { + // Throw if this is an error other than "not found". + throw lookup + } + // Otherwise it is a Boom.notFound(), proceed ahead. + } + await sbp('chelonia/db/set', namespaceKey(name), value) + return { name, value } + }, + 'backend/db/lookupName': async function (name: string) { + const value = await sbp('chelonia/db/get', namespaceKey(name)) + return value || new NotFound(name) + }, + // ======================= + // Filesystem API + // + // TODO: add encryption + // ======================= + 'backend/db/readFile': async function (filename: string) { + const filepath = throwIfFileOutsideDataDir(filename) + if (!(await fileExists(filepath))) { + return new NotFound() + } + return await readFileAsync(filepath) + }, + 'backend/db/writeFile': async function (filename: string, data: any) { + // TODO: check for how much space we have, and have a server setting + // that determines how much of the disk space we're allowed to + // use. If the size of the file would cause us to exceed this + // amount, throw an exception + return await writeFileAsync(throwIfFileOutsideDataDir(filename), data) + }, + 'backend/db/writeFileOnce': async function (filename: string, data: any) { + const filepath = throwIfFileOutsideDataDir(filename) + if (await fileExists(filepath)) { + console.warn('writeFileOnce: exists:', filepath) + return + } + return await writeFileAsync(filepath, data) + } +})) + +function namespaceKey (name: string): string { + return 'name=' + name +} + +function throwIfFileOutsideDataDir (filename: string): string { + const filepath = pathlib.resolve(pathlib.join(dataFolder, filename)) + if (!filepath.startsWith(dataFolder)) { + throw new BadRequest(`bad name: ${filename}`) + } + return filepath +} + +if (production || Deno.env.get('GI_PERSIST')) { + sbp('sbp/selectors/overwrite', { + // we cannot simply map this to readFile, because 'chelonia/db/getEntry' + // calls this and expects a string, not a Buffer + // 'chelonia/db/get': sbp('sbp/selectors/fn', 'backend/db/readFile'), + 'chelonia/db/get': async function (filename: string) { + const value = await sbp('backend/db/readFile', filename) + return value instanceof Error ? null : value.toString('utf8') + }, + 'chelonia/db/set': sbp('sbp/selectors/fn', 'backend/db/writeFile') + }) + sbp('sbp/selectors/lock', ['chelonia/db/get', 'chelonia/db/set', 'chelonia/db/delete']) +} diff --git a/deno-backend/events.ts b/deno-backend/events.ts new file mode 100644 index 0000000000..e1aa4ed860 --- /dev/null +++ b/deno-backend/events.ts @@ -0,0 +1 @@ +export const SERVER_RUNNING = 'server-running' diff --git a/deno-backend/index.ts b/deno-backend/index.ts new file mode 100644 index 0000000000..c80bf8e883 --- /dev/null +++ b/deno-backend/index.ts @@ -0,0 +1,73 @@ +import { bold } from "fmt/colors.ts" + +import sbp from "@sbp/sbp" +import "@sbp/okturtles.data" +import "@sbp/okturtles.events" + +import { SERVER_RUNNING } from './events.ts' +import { PUBSUB_INSTANCE } from './instance-keys.ts' + +const logger = window.logger = function (err) { + console.error(err) + err.stack && console.error(err.stack) + return err // routes.js is written in a way that depends on this returning the error +} + +const dontLog = { 'backend/server/broadcastEntry': true } + +function logSBP (domain, selector, data) { + if (!dontLog[selector]) { + console.log(bold(`[sbp] ${selector}`), data) + } +} + +;['backend'].forEach(domain => sbp('sbp/filters/domain/add', domain, logSBP)) +;[].forEach(sel => sbp('sbp/filters/selector/add', sel, logSBP)) + +export default (new Promise((resolve, reject) => { + sbp('okTurtles.events/on', SERVER_RUNNING, function () { + console.log(bold('backend startup sequence complete.')) + resolve() + }) + // Call this after we've registered listener for `SERVER_RUNNING`. + import('./server.ts') +})) + +const shutdownFn = function (message) { + sbp('okTurtles.data/apply', PUBSUB_INSTANCE, function (pubsub) { + console.log('message received in child, shutting down...', message) + pubsub.on('close', async function () { + try { + await sbp('backend/server/stop') + console.log('Backend server down') + process.send({}) // tell grunt we've successfully shutdown the server + process.nextTick(() => Deno.exit(0)) // triple-check we quit :P + } catch (err) { + console.error('Error during shutdown:', err) + Deno.exit(1) + } + }) + pubsub.close() + // Since `ws` v8.0, `WebSocketServer.close()` no longer closes remaining connections. + // See https://github.com/websockets/ws/commit/df7de574a07115e2321fdb5fc9b2d0fea55d27e8 + pubsub.clients.forEach(client => client.terminate()) + }) +} + +// Sent by Nodemon. +addEventListener('SIGUSR2', shutdownFn) + +// When spawned by another process, +// listen for message events to cleanly shutdown and relinquish port. +addEventListener('message', shutdownFn) + +// Equivalent to the `uncaughtException` event in Nodejs. +addEventListener('error', (event) => { + console.error('[server] Unhandled exception:', event) + Deno.exit(1) +}) + +addEventListener('unhandledRejection', (reason, p) => { + console.error('[server] Unhandled promise rejection:', p, 'reason:', reason) + Deno.exit(1) +}) diff --git a/deno-backend/instance-keys.ts b/deno-backend/instance-keys.ts new file mode 100644 index 0000000000..d371506154 --- /dev/null +++ b/deno-backend/instance-keys.ts @@ -0,0 +1,2 @@ +export const SERVER_INSTANCE = '@instance/server' +export const PUBSUB_INSTANCE = '@instance/pubsub' diff --git a/deno-backend/pubsub.ts b/deno-backend/pubsub.ts new file mode 100644 index 0000000000..437a708509 --- /dev/null +++ b/deno-backend/pubsub.ts @@ -0,0 +1,381 @@ +import { + acceptWebSocket, + isWebSocketCloseEvent, + isWebSocketPingEvent, +} from "https://deno.land/std@0.92.0/ws/mod.ts"; + +import { messageParser } from '~/shared/pubsub.ts' + +const CI = Deno.env.get('CI') +const NODE_ENV = Deno.env.get('NODE_ENV') + +const emptySet = Object.freeze(new Set()) +// Used to tag console output. +const tag = '[pubsub]' + +// ====== Helpers ====== // + +// Only necessary when using the `ws` module in Deno. + +const generateSocketID = (() => { + let counter = 0 + + return (debugID: string) => String(counter++) + (debugID ? '-' + debugID : '') +})() + +const log: Function = console.log.bind(console, tag) +log.debug = console.debug.bind(console, tag) +log.error = console.error.bind(console, tag) + +// ====== API ====== // + +export function createErrorResponse (data: Object): string { + return JSON.stringify({ type: 'error', data }) +} + +export function createMessage (type: string, data: JSONType): string { + return JSON.stringify({ type, data }) +} + +export function createNotification (type: string, data: Object): string { + return JSON.stringify({ type, data }) +} + +export function createResponse (type: string, data: Object): string { + return JSON.stringify({ type, data }) +} + +export function createServer(options?: Object = {}) { + const server = { + clients: new Set(), + customServerEventHandlers: Object.create(null), + customSocketEventHandlers: Object.create(null), + handleUpgradeableRequest, + messageHandlers: { ...defaultMessageHandlers, ...options.customMessageHandlers }, + options: { ...defaultOptions, ...options }, + get port () { return rawHttpServer.listener?.addr.port }, + queuesByEventName: new Map(), + subscribersByContractID: Object.create(null), + }; + + function handleUpgradeableRequest(request: Request): Response { + const { socket, response } = Deno.upgradeWebSocket(request) + // Our code + socket.onopen = () => { + server.clients.add(socket); + server.emit('connection', socket, request) + }; + return response; + } + + server.emit = (name, ...args) => { + console.log('emit:', name) + const queue = server.queuesByEventName.get(name) ?? emptySet; + try { + for(const callback of queue) { + Function.prototype.call.call(callback, server, ...args) + } + } catch (error) { + if(server.queuesByEventName.has('error')) { + server.emit('error', error); + } else { + throw error; + } + } + }; + + server.off = (name, callback) => { + const queue = server.queuesByEventName.get(name) ?? emptySet; + queue.delete(callback); + }; + + server.on = (name, callback) => { + if(!server.queuesByEventName.has(name)) { + server.queuesByEventName.set(name, new Set()); + } + const queue = server.queuesByEventName.get(name); + queue.add(callback); + }; + + // Add listeners for server events, i.e. events emitted on the server object. + Object.keys(internalServerHandlers).forEach((name) => { + server.on(name, (...args) => { + try { + // Always call the default handler first. + internalServerHandlers[name]?.call(server, ...args) + server.customServerEventHandlers[name]?.call(server, ...args) + } catch (error) { + server.emit('error', error) + } + }) + }) + // Setup a ping interval if required. + if (server.options.pingInterval > 0) { + server.pingIntervalID = setInterval(() => { + if (server.clients.size && server.options.logPingRounds) { + log.debug('Pinging clients') + } + server.clients.forEach((client) => { + if (client.pinged && !client.activeSinceLastPing) { + log(`Disconnecting irresponsive client ${client.id}`) + return client.terminate() + } + if (client.readyState === WebSocket.OPEN) { + client.send(createMessage(PING, Date.now()), () => { + client.activeSinceLastPing = false + client.pinged = true + }) + console.log('client pinged') + } + }) + }, server.options.pingInterval) + } + return Object.assign(server, publicMethods) +} + +export function isUpgradeableRequest (request: Request): boolean { + const upgrade = request.headers.get('upgrade') + if (upgrade?.toLowerCase() === "websocket") return true + return false +} + +const defaultOptions = { + logPingRounds: true, + logPongMessages: true, + maxPayload: 6 * 1024 * 1024, + pingInterval: 0 // 30000 +} + +// Internal default handlers for server events. +// The `this` binding refers to the server object. +const internalServerHandlers = { + close () { + log('Server closed') + }, + /** + * Emitted when a connection handshake completes. + * + * @see https://github.com/websockets/ws/blob/master/doc/ws.md#event-connection + * @param {ws.WebSocket} socket - The client socket that connected. + * @param {http.IncomingMessage} request - The underlying Node http GET request. + */ + connection (socket: Object, request: Object) { + console.log('connection:', request.url) + const server = this + const url = request.url + const urlSearch = url.includes('?') ? url.slice(url.lastIndexOf('?')) : '' + const debugID = new URLSearchParams(urlSearch).get('debugID') || '' + socket.id = generateSocketID(debugID) + socket.activeSinceLastPing = true + socket.pinged = false + socket.server = server + socket.subscriptions = new Set() + + log(`Socket ${socket.id} connected. Total: ${this.clients.size}`) + + // Add listeners for socket events, i.e. events emitted on a socket object. + if (!server.usingLegacyDenoWS) { + ['close', 'error', 'message', 'ping', 'pong'].forEach((eventName) => { + socket.addEventListener(eventName, (...args) => { + // Logging of 'message' events is handled in the default 'message' event handler. + if (eventName !== 'message') { + log(`Event '${eventName}' on socket ${socket.id}`, ...args.map(arg => String(arg))) + } + try { + (internalSocketEventHandlers)[eventName]?.call(socket, ...args) + server.customSocketEventHandlers[eventName]?.call(socket, ...args) + } catch (error) { + server.emit('error', error) + server.terminateSocket(socket) + } + }) + }) + } + }, + error (error: Error) { + log.error('Server error:', error) + }, + headers () { + }, + listening () { + log('Server listening') + } +} + +// Default handlers for server-side client socket events. +// The `this` binding refers to the connected `ws` socket object. +const internalSocketEventHandlers = { + close (code: string, reason: string) { + const socket = this + const { server, id: socketID } = this + + // Notify other client sockets that this one has left any room they shared. + for (const contractID of socket.subscriptions) { + const subscribers = server.subscribersByContractID[contractID] + // Remove this socket from the subscribers of the given contract. + subscribers.delete(socket) + const notification = createNotification(UNSUB, { contractID, socketID }) + server.broadcast(notification, { to: subscribers }) + } + socket.subscriptions.clear() + // Additional code. + socket.server.clients.delete(socket) + }, + + message (event: MessageEvent) { + const socket = this + const { server } = this + const { type, data } = server.usingLegacyDenoWS ? { type: 'message', data: event } : event + const text = data + let msg: Message = { type: '' } + + try { + console.log('data:', data) + msg = messageParser(data) + } catch (error) { + log.error(`Malformed message: ${error.message}`) + server.rejectMessageAndTerminateSocket(msg, socket) + return + } + // Now that we have successfully parsed the message, we can log it. + if (msg.type !== 'pong' || server.options.logPongMessages) { + log(`Received '${msg.type}' on socket ${socket.id}`, text) + } + // The socket can be marked as active since it just received a message. + socket.activeSinceLastPing = true + const handler = server.messageHandlers[msg.type] + + if (handler) { + try { + handler.call(socket, msg) + } catch (error) { + // Log the error message and stack trace but do not send it to the client. + log.error(error) + server.rejectMessageAndTerminateSocket(msg, socket) + } + } else { + log.error(`Unhandled message type: ${msg.type}`) + server.rejectMessageAndTerminateSocket(msg, socket) + } + } +} + +export const NOTIFICATION_TYPE = { + APP_VERSION: 'app-version', + ENTRY: 'entry' +} + +const PING = 'ping' +const PONG = 'pong' +const PUB = 'pub' +const SUB = 'sub' +const UNSUB = 'unsub' +const SUCCESS = 'success' + +// These handlers receive the connected `ws` socket through the `this` binding. +const defaultMessageHandlers = { + [PONG] (msg: Message) { + const socket = this + // const timestamp = Number(msg.data) + // const latency = Date.now() - timestamp + socket.activeSinceLastPing = true + }, + + [PUB] (msg: Message) { + // Currently unused. + }, + + [SUB] ({ contractID, dontBroadcast }: SubMessage) { + const socket = this + const { server, id: socketID } = this + + if (!socket.subscriptions.has(contractID)) { + // Add the given contract ID to our subscriptions. + socket.subscriptions.add(contractID) + if (!server.subscribersByContractID[contractID]) { + server.subscribersByContractID[contractID] = new Set() + } + const subscribers = server.subscribersByContractID[contractID] + // Add this socket to the subscribers of the given contract. + subscribers.add(socket) + if (!dontBroadcast) { + // Broadcast a notification to every other open subscriber. + const notification = createNotification(SUB, { contractID, socketID }) + server.broadcast(notification, { to: subscribers, except: socket }) + } + } else { + log('Already subscribed to', contractID) + } + socket.send(createResponse(SUCCESS, { type: SUB, contractID })) + }, + + [UNSUB] ({ contractID, dontBroadcast }: UnsubMessage) { + const socket = this + const { server, id: socketID } = this + + if (socket.subscriptions.has(contractID)) { + // Remove the given contract ID from our subscriptions. + socket.subscriptions.delete(contractID) + if (server.subscribersByContractID[contractID]) { + const subscribers = server.subscribersByContractID[contractID] + // Remove this socket from the subscribers of the given contract. + subscribers.delete(socket) + if (!dontBroadcast) { + const notification = createNotification(UNSUB, { contractID, socketID }) + // Broadcast a notification to every other open subscriber. + server.broadcast(notification, { to: subscribers, except: socket }) + } + } + } else { + log('Was not subscribed to', contractID) + } + socket.send(createResponse(SUCCESS, { type: UNSUB, contractID })) + } +} + +const publicMethods = { + /** + * Broadcasts a message, ignoring clients which are not open. + * + * @param message + * @param to - The intended recipients of the message. Defaults to every open client socket. + * @param except - A recipient to exclude. Optional. + */ + broadcast ( + message: Message, + { to, except }: { to?: Iterable, except?: Object } + ) { + const server = this + + for (const client of to || server.clients) { + if (client.readyState === WebSocket.OPEN && client !== except) { + client.send(message) + } + } + }, + + // Enumerates the subscribers of a given contract. + * enumerateSubscribers (contractID: string): Iterable { + const server = this + + if (contractID in server.subscribersByContractID) { + yield * server.subscribersByContractID[contractID] + } + }, + + rejectMessageAndTerminateSocket (request: Message, socket: Object) { + socket.send(createErrorResponse({ ...request }), () => this.terminateSocket(socket)) + }, + + terminateSocket (socket: Object) { + const server = this + internalSocketEventHandlers.close.call(socket) + + // Remove listeners for socket events, i.e. events emitted on a socket object. + ;['close', 'error', 'message', 'ping', 'pong'].forEach((eventName) => { + socket.removeEventListener(eventName, (internalSocketEventHandlers)[eventName]) + socket.removeEventListener(eventName, server.customSocketEventHandlers[eventName]) + }) + socket.close() + }, +} diff --git a/deno-backend/router.ts b/deno-backend/router.ts new file mode 100644 index 0000000000..f454a54126 --- /dev/null +++ b/deno-backend/router.ts @@ -0,0 +1,363 @@ +import { + NormalizedRoute, + RequestParams, + Route, + RouteHandler, + RouteOptions, + MatchedRoute +} from './types.ts'; + +export type RouteOptionsHasHandler = RouteOptions & Required>; +export type RouteOptionsHasMethod = RouteOptions & Required>; +export type RouteOptionsHasPath = RouteOptions & Required>; +export type RouteOptionsHasHandlerAndMethod = RouteOptions & Required>; +export type RouteOptionsHasHandlerAndPath = RouteOptions & Required>; +export type RouteOptionsHasMethodAndPath = RouteOptions & Required>; +export type RequiredRouteOptions = RouteOptions & Required>; + +export type RoutesList = RequiredRouteOptions | Router | Iterable; +export type RoutesListHasHandler = RouteOptionsHasHandler | Router | Iterable; +export type RoutesListHasMethod = RouteOptionsHasMethod | Router | Iterable; +export type RoutesListHasPath = RouteOptionsHasPath | Router | string | Iterable; +export type RoutesListHasHandlerAndMethod = RouteOptionsHasHandlerAndMethod | Router | Iterable; +export type RoutesListHasHandlerAndPath = RouteOptionsHasHandlerAndPath | Router | Iterable; +export type RoutesListHasMethodAndPath = RouteOptionsHasMethodAndPath | Router | Iterable; + +const paramPattern = /\{(\w+)(?:\?|\*(?:[1-9]\d*)?)?\}/u; +const paramsPattern = new RegExp(paramPattern, paramPattern.flags + 'g'); + +const expandPath = (path: string): Array => { + return Array.from(path.matchAll(paramsPattern) as Iterable).flatMap((match) => { + const [param, name] = match; + const before = match.input.slice(0, match.index); + const after = match.input.slice(match.index + param.length); + + // Optional param, expand to paths WITH and WITHOUT the param + if (param.endsWith('?}')) { + const isWholeSegment = before.endsWith('/') && after.startsWith('/'); + const withParam = before + `{${name}}` + after; + const withoutParam = before + (isWholeSegment ? after.slice(1) : after) + return [withParam, withoutParam]; + } + + return []; + }); +}; + +const isDynamicSegment = (segment: string): boolean => { + return segment.startsWith('{') && segment.endsWith('}') && paramPattern.test(segment); +}; + +const getParamName = (segment: string): string => { + const param = segment.match(paramPattern); + return param ? param[1] : ''; +}; + +const toPathfinder = (segments: Array): string => { + const replacePart = (str: string) => { + return str && '.'; + }; + return segments.map(replacePart).join('/'); +}; + +/** + * Returns a human friendly text representation of the given route, such as GET /foo + */ +const toSignature = (route: NormalizedRoute): string => { + return route.method + ' ' + (route.vhost || '') + route.path; +}; + +const fingerprintPath = (path: string): string => { + return path.replace(paramsPattern, (param) => { + return param.endsWith('*}') ? '#' : '?'; + }); +}; + +const toConflictId = (route: NormalizedRoute): string => { + return toSignature({ + ...route, + path : fingerprintPath(route.path) + }); +} + +const sortRoutes = (left: NormalizedRoute, right: NormalizedRoute): number => { + const leftFirst = -1; + const rightFirst = 1; + const unchanged = 0; + + if (left.segments.filter(isDynamicSegment).length < + right.segments.filter(isDynamicSegment).length) { + return leftFirst; + } + if (left.segments.filter(isDynamicSegment).length > + right.segments.filter(isDynamicSegment).length) { + return rightFirst; + } + + if (left.segments.length < right.segments.length) { + return leftFirst; + } + if (left.segments.length > right.segments.length) { + return rightFirst; + } + + if (left.path < right.path) { + return leftFirst; + } + if (left.path > right.path) { + return rightFirst; + } + + return unchanged; +}; + +interface RoutingTable { + conflictIds: Map, + list: Array, + pathfinders: Map>, + wildcards: Array +} + +/** + * A router represents a collection of routes and determines which route will handle a given HTTP request. + * Use `pogo.router()` to create a router instance. + */ +export default class Router { + routes: RoutingTable; + constructor(route?: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler) { + this.routes = { + conflictIds : new Map(), + list : [], + pathfinders : new Map(), + wildcards : [] + }; + if (route) { + this.add(route, options, handler); + } + } + add(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; + add(route: RoutesListHasMethodAndPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; + add(route: RoutesListHasHandlerAndMethod, options: RouteOptionsHasPath, handler?: RouteHandler): this; + add(route: RoutesListHasHandlerAndPath, options: RouteOptionsHasMethod, handler?: RouteHandler): this; + add(route: RoutesListHasHandler, options: RouteOptionsHasMethodAndPath, handler?: RouteHandler): this; + add(route: RoutesListHasPath, options: RouteOptionsHasHandlerAndMethod, handler?: RouteHandler): this; + add(route: RoutesListHasPath, options: RouteOptionsHasMethod, handler: RouteHandler): this; + add(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; + add(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; + add(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { + if (route && typeof route === 'object' && Symbol.iterator in route) { + for (const settings of route as Iterable) { + this.add(settings, options, handler); + } + return this; + } + if (route instanceof Router) { + this.add(route.routes.list); + return this; + } + + const normalizedRoute = { + ...(typeof route === 'string' ? { path : route } : route), + ...(typeof options === 'function' ? { handler : options } : options), + ...(handler ? { handler } : null) + } as Route; + + if (typeof normalizedRoute.method === 'object' && Symbol.iterator in normalizedRoute.method) { + for (const method of normalizedRoute.method as Iterable) { + this.add({ + ...normalizedRoute, + method + }); + } + return this; + } + + if (typeof normalizedRoute.path === 'object' && Symbol.iterator in normalizedRoute.path) { + for (const path of normalizedRoute.path as Iterable) { + this.add({ + ...normalizedRoute, + path + }); + } + return this; + } + + const expandedPaths = expandPath(normalizedRoute.path); + if (expandedPaths.length > 0) { + this.add({ + ...normalizedRoute, + path : expandedPaths + }); + return this; + } + + const record: NormalizedRoute = { + ...normalizedRoute, + method : normalizedRoute.method.toUpperCase(), + paramNames : Array.from(normalizedRoute.path.matchAll(paramsPattern), (match) => { + return match[1]; + }), + segments : normalizedRoute.path.split('/'), + }; + + const conflictId = toConflictId(record); + const existingRoute = this.routes.conflictIds.get(conflictId); + if (existingRoute) { + const newRoute = toSignature(record); + const oldRoute = toSignature(existingRoute); + throw new Error(`Route conflict: new route "${newRoute}" conflicts with existing route "${oldRoute}"`); + } + + this.routes.conflictIds.set(conflictId, record); + + const hasWildcardParam = /\{\w+\*\}/u.test(record.path); + if (hasWildcardParam) { + this.routes.wildcards.push(record); + this.routes.wildcards.sort(sortRoutes); + } + else { + const pathfinder = toPathfinder(record.segments); + const pathfinderRoutes = this.routes.pathfinders.get(pathfinder) ?? []; + pathfinderRoutes.push(record); + pathfinderRoutes.sort(sortRoutes); + this.routes.pathfinders.set(pathfinder, pathfinderRoutes); + } + + this.routes.list.push(record); + this.routes.list.sort(sortRoutes); + + return this; + } + all(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; + all(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; + all(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; + all(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; + all(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; + all(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; + all(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { + const config = { + ...(typeof options === 'function' ? { handler : options } : options), + method : '*' + }; + this.add(route, config, handler); + return this; + } + delete(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; + delete(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; + delete(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; + delete(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; + delete(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; + delete(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; + delete(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { + const config = { + ...(typeof options === 'function' ? { handler : options } : options), + method : 'DELETE' + }; + this.add(route, config, handler); + return this; + } + get(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; + get(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; + get(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; + get(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; + get(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; + get(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; + get(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { + const config = { + ...(typeof options === 'function' ? { handler : options } : options), + method : 'GET' + }; + this.add(route, config, handler); + return this; + } + patch(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; + patch(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; + patch(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; + patch(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; + patch(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; + patch(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; + patch(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { + const config = { + ...(typeof options === 'function' ? { handler : options } : options), + method : 'PATCH' + }; + this.add(route, config, handler); + return this; + } + post(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; + post(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; + post(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; + post(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; + post(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; + post(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; + post(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { + const config = { + ...(typeof options === 'function' ? { handler : options } : options), + method : 'POST' + }; + this.add(route, config, handler); + return this; + } + put(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; + put(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; + put(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; + put(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; + put(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; + put(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; + put(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { + const config = { + ...(typeof options === 'function' ? { handler : options } : options), + method : 'PUT' + }; + this.add(route, config, handler); + return this; + } + lookup(method: string, path: string, host?: string): MatchedRoute | undefined { + const pathSegments = path.split('/'); + const pathfinder = toPathfinder(pathSegments); + + const matchRoute = (list: Array = []): NormalizedRoute | undefined => { + return list.find((route: NormalizedRoute) => { + const isMethodMatch = route.method === method || route.method === '*'; + if (!isMethodMatch) { + return false; + } + const isHostMatch = !host || !route.vhost || route.vhost === host; + if (!isHostMatch) { + return false; + } + const isStaticPath = route.paramNames.length === 0; + if (isStaticPath) { + return route.path === path; + } + + const matchesAllSegments = route.segments.every((routeSegment: string, index: number): boolean => { + return isDynamicSegment(routeSegment) || (routeSegment === pathSegments[index]); + }); + + const isPathMatch = matchesAllSegments && ((route.segments.length === pathSegments.length) || route.segments[route.segments.length - 1].endsWith('*}')); + + return isPathMatch; + }); + } + + const candidates = this.routes.pathfinders.get(pathfinder); + const wildcardRoutes = this.routes.wildcards; + const route = matchRoute(candidates) || matchRoute(wildcardRoutes); + + return route && { + ...route, + params : route.segments.reduce((params: RequestParams, routeSegment: string, index: number) => { + if (isDynamicSegment(routeSegment)) { + const name = getParamName(routeSegment); + params[name] = routeSegment.endsWith('*}') ? pathSegments.slice(index).join('/') : pathSegments[index]; + } + return params; + }, {}) + }; + } +} + +export { + toSignature +}; diff --git a/deno-backend/routes.ts b/deno-backend/routes.ts new file mode 100644 index 0000000000..8903fac371 --- /dev/null +++ b/deno-backend/routes.ts @@ -0,0 +1,197 @@ +import sbp from '@sbp/sbp' +import { GIMessage } from '~/shared/domains/chelonia/GIMessage.ts' +import { blake32Hash } from '~/shared/functions.ts' +import { SERVER_INSTANCE } from './instance-keys.ts' + +import { badRequest } from 'pogo/lib/bang.ts' +import Router from 'pogo/lib/router.ts' + +import './database.ts' +import * as pathlib from 'path' + +export const router = new Router() + +const route = new Proxy({}, { + get: function (obj, prop) { + return function (path: string, handler: Function | Object) { + router.add({ path, method: prop, handler }) + } + } +}) + +// RESTful API routes + +// NOTE: We could get rid of this RESTful API and just rely on pubsub.js to do this +// —BUT HTTP2 might be better than websockets and so we keep this around. +// See related TODO in pubsub.js and the reddit discussion link. +route.POST('/event', async function (request, h) { + try { + console.log('/event handler') + const payload = await request.raw.text(); + console.log("payload:", payload); + + const entry = GIMessage.deserialize(payload) + await sbp('backend/server/handleEntry', entry) + return entry.hash() + } catch (err) { + if (err.name === 'ChelErrorDBBadPreviousHEAD') { + console.error(bold(yellow('ChelErrorDBBadPreviousHEAD')), err) + return badRequest(err.message) + } + console.error(err) + return err + } +}) + +route.GET('/events/{contractID}/{since}', async function (request, h) { + try { + const { contractID, since } = request.params + const json = await sbp('backend/db/streamEntriesSince', contractID, since) + // Make sure to close the stream in case of disconnection." + // request.events.once('disconnect', stream.cancel.bind(stream)) + return h.response(json).type('application/json') + } catch (err) { + return logger(err) + } +}) + +route.POST('/name', async function (request, h) { + try { + console.debug('/name', request.body) + const payload = await request.raw.json(); + + const { name, value } = payload + return await sbp('backend/db/registerName', name, value) + } catch (err) { + return logger(err) + } +}) + +route.GET('/name/{name}', async function (request, h) { + console.debug('GET /name/{name}', request.params.name) + try { + const result = await sbp('backend/db/lookupName', request.params.name) + return result instanceof Deno.errors.NotFound ? request.response.code(404) : result + } catch (err) { + return (err) + } +}) + +route.GET('/latestHash/{contractID}', async function handler (request, h) { + try { + const { contractID } = request.params + const hash = await sbp('chelonia/db/latestHash', contractID) + console.debug(`[backend] latestHash for ${contractID}: `, hash) + request.response.header('cache-control', 'no-store') + if (!hash) { + console.warn(`[backend] latestHash not found for ${contractID}`) + return new NotFound() + } + return hash + } catch (err) { + return logger(err) + } +}) + +route.GET('/time', function (request, h) { + request.response.header('cache-control', 'no-store') + return new Date().toISOString() +}) + +// file upload related + +// TODO: if the browser deletes our cache then not everyone +// has a complete copy of the data and can act as a +// new coordinating server... I don't like that. + +const MEGABYTE = 1048576 // TODO: add settings for these +const SECOND = 1000 + +// TODO: only allow uploads from registered users +const fileUploadOptions = { + output: 'data', + multipart: true, + allow: 'multipart/form-data', + maxBytes: 6 * MEGABYTE, // TODO: make this a configurable setting + timeout: 10 * SECOND // TODO: make this a configurable setting +} + +route.POST('/file', async function (request, h) { + try { + console.log('FILE UPLOAD!') + + const formData = await request.raw.formData() + const data = formData.get('data') + const hash = formData.get('hash') + if (!data) return badRequest('missing data') + if (!hash) return badRequest('missing hash') + + const fileData = await new Promise((resolve, reject) => { + const fileReader = new FileReader() + fileReader.onload = (event) => { + resolve(fileReader.result) + } + fileReader.onerror = (event) => { + reject(fileReader.error) + } + fileReader.readAsArrayBuffer(data) + }) + const ourHash = blake32Hash(new Uint8Array(fileData)) + if (ourHash !== hash) { + console.error(`hash(${hash}) != ourHash(${ourHash})`) + return new badRequest('bad hash!') + } + await sbp('backend/db/writeFileOnce', hash, fileData) + console.log('/file/' + hash); + return '/file/' + hash + } catch (err) { + console.error(err) + return err + } +}) + +route.GET('/file/{hash}', async function handler (request, h) { + try { + const { hash } = request.params + const base = pathlib.resolve('data') + console.debug(`GET /file/${hash}`) + console.debug(base) + // Reusing the given `hash` parameter to set the ETag should be faster than + // letting Hapi hash the file to compute an ETag itself. + return (await h.file(pathlib.join(base, hash))) + .header('content-type', 'application/octet-stream') + .header('cache-control', 'public,max-age=31536000,immutable') + .header('etag', `"${hash}"`) + .header('last-modified', new Date().toGMTString()) + } catch (err) { + console.log(err) + } +}) + +// SPA routes + +route.GET('/assets/{subpath*}', function handler (request, h) { + const { subpath } = request.params + const basename = pathlib.basename(subpath) + console.debug(`GET /assets/${subpath}`) + const base = pathlib.resolve('dist/assets') + // In the build config we told our bundler to use the `[name]-[hash]-cached` template + // to name immutable assets. This is useful because `dist/assets/` currently includes + // a few files without hash in their name. + if (basename.includes('-cached')) { + return h.file(pathlib.join(base, subpath)) + .header('etag', basename) + .header('cache-control', 'public,max-age=31536000,immutable') + } + // Files like `main.js` or `main.css` should be revalidated before use. Se we use the default headers. + // This should also be suitable for serving unversioned fonts and images. + return h.file(pathlib.join(base, subpath)) +}) + +route.GET('/app/{path*}', function handler (req, h) { + return h.file(pathlib.resolve('./dist/index.html')) +}) + +route.GET('/', function (req, h) { + return h.redirect('/app/') +}) diff --git a/deno-backend/server.ts b/deno-backend/server.ts new file mode 100644 index 0000000000..8fe8e82a15 --- /dev/null +++ b/deno-backend/server.ts @@ -0,0 +1,108 @@ +import { blue, bold, gray } from "fmt/colors.ts" + +import * as http from 'https://deno.land/std@0.132.0/http/server.ts'; +import pogo from 'pogo/main.ts'; + +import sbp from "@sbp/sbp" +import GiAuth from './auth.ts' +import './database.ts' +import { SERVER_RUNNING } from './events.ts' +import { SERVER_INSTANCE, PUBSUB_INSTANCE } from './instance-keys.ts' +import { + createMessage, + createNotification, + createServer, + isUpgradeableRequest, + NOTIFICATION_TYPE, +} from '~/backend/pubsub.ts' +import { router } from './routes.ts' + +import { GIMessage } from '~/shared/domains/chelonia/GIMessage.ts' + +const { version } = await import('~/package.json', { + assert: { type: "json" }, +}) + +const applyPortShift = (env) => { + // TODO: implement automatic port selection when `PORT_SHIFT` is 'auto'. + const API_PORT = 8000 + Number.parseInt(env.PORT_SHIFT || '0') + const API_URL = 'http://127.0.0.1:' + API_PORT + + if (Number.isNaN(API_PORT) || API_PORT < 8000 || API_PORT > 65535) { + throw new RangeError(`Invalid API_PORT value: ${API_PORT}.`) + } + return { ...env, API_PORT: String(API_PORT), API_URL: String(API_URL) } +} + +for (const [key, value] of Object.entries(applyPortShift(Deno.env.toObject()))) { + Deno.env.set(key, value) +} + +Deno.env.set('GI_VERSION', `${version}@${new Date().toISOString()}`) + +const API_PORT = Deno.env.get('API_PORT') +const API_URL = Deno.env.get('API_URL') +const CI = Deno.env.get('CI') +const GI_VERSION = Deno.env.get('GI_VERSION') +const NODE_ENV = Deno.env.get('NODE_ENV') + +const pubsub = createServer({ + serverHandlers: { + connection (socket: Object, request: Object) { + if (NODE_ENV === 'production') { + socket.send(createNotification(NOTIFICATION_TYPE.APP_VERSION, GI_VERSION)) + } + } + } +}) + +const pogoServer = pogo.server({ + hostname: 'localhost', + port: Number.parseInt(API_PORT), +}) + +// Patch the Pogo server to add WebSocket support. +{ + const originalInject = pogoServer.inject.bind(pogoServer) + + pogoServer.inject = (request) => { + if (isUpgradeableRequest(request)) { + return pubsub.handleUpgradeableRequest(request) + } else { + return originalInject(request) + } + } +} +pogoServer.router = router + +console.log('Backend HTTP server listening:', pogoServer.options) + +if (NODE_ENV === 'development' && !CI) { + server.events?.on('response', (request, event, tags) => { + console.debug(grey(`${request.info.remoteAddress}: ${request.method.toUpperCase()} ${request.path} --> ${request.response.statusCode}`)) + }) +} + +sbp('okTurtles.data/set', PUBSUB_INSTANCE, pubsub) +sbp('okTurtles.data/set', SERVER_INSTANCE, pogoServer) + +sbp('sbp/selectors/register', { + 'backend/server/broadcastEntry': async function (entry: GIMessage) { + const pubsub = sbp('okTurtles.data/get', PUBSUB_INSTANCE) + const pubsubMessage = createMessage(NOTIFICATION_TYPE.ENTRY, entry.serialize()) + const subscribers = pubsub.enumerateSubscribers(entry.contractID()) + console.log(blue(bold(`[pubsub] Broadcasting ${entry.description()}`))) + await pubsub.broadcast(pubsubMessage, { to: subscribers }) + }, + 'backend/server/handleEntry': async function (entry: GIMessage) { + await sbp('chelonia/db/addEntry', entry) + await sbp('backend/server/broadcastEntry', entry) + }, + 'backend/server/stop': function () { + return pogoServer.stop() + } +}) + +pogoServer.start() + .then(() => sbp('okTurtles.events/emit', SERVER_RUNNING, pogoServer)) + .catch(console.error.bind(console, 'error in server.start():')) diff --git a/deno-backend/types.ts b/deno-backend/types.ts new file mode 100644 index 0000000000..7a82ef0c99 --- /dev/null +++ b/deno-backend/types.ts @@ -0,0 +1,36 @@ +import Request from './request.ts'; +import ServerResponse from './response.ts'; +import Toolkit from './toolkit.ts'; + +export interface Route { + method: string, + path: string, + handler: RouteHandler, + vhost?: string +} + +export type RequestParams = { [param: string]: string }; +export type RequestState = { [name: string]: string }; + +export interface RouteOptions extends Omit, 'method' | 'path'> { + method?: Route['method'] | Iterable, + path?: Route['path'] | Iterable +} + +export interface NormalizedRoute extends Route { + paramNames: Array, + segments: Array +} + +export interface MatchedRoute extends NormalizedRoute { + params: RequestParams +} + +type JSONStringifyable = boolean | null | number | object | string; +export type ResponseBody = Deno.Reader | Uint8Array | JSONStringifyable; +export type RouteHandlerResult = ServerResponse | ResponseBody | Error | Promise; +export type RouteHandler = (request: Request, h: Toolkit) => RouteHandlerResult; + +export type { ServerOptions } from './server.ts'; +export type { FileHandlerOptions } from './helpers/file.ts'; +export type { DirectoryHandlerOptions } from './helpers/directory.tsx'; diff --git a/deno-shared/declarations.ts b/deno-shared/declarations.ts new file mode 100644 index 0000000000..0e0d9dc7a9 --- /dev/null +++ b/deno-shared/declarations.ts @@ -0,0 +1,66 @@ +/* eslint no-undef: "off", no-unused-vars: "off" */ +// ======================= +// This file prevents flow from bitching about globals and "Required module not found" +// https://github.com/facebook/flow/issues/2092#issuecomment-232917073 +// +// Note that the modules can (and should) be properly fixed with flow-typed +// https://github.com/okTurtles/group-income/issues/157 +// ======================= + +// TODO: create a script in scripts/ to run flow via grunt-exec +// and have it output (at the end of the run) helpful suggestions +// like how to use `declare module` to ignore .vue requires, +// and also a strong urging to not overdue the types because +// FlowType is a little bit stupid and it can turn into a +// banging-head-on-desk timesink (literally those words). +// Have the script explain which files represent what. + +// Our globals. +declare function logger(err: Error): void +// Nodejs globals. +declare var process: any + +// ======================= +// Fix "Required module not found" in a hackish way. +// TODO: Proper fix is to use: +// https://github.com/okTurtles/group-income/issues/157 +// ======================= +declare module '@hapi/boom' { declare module.exports: any } +declare module '@hapi/hapi' { declare module.exports: any } +declare module '@hapi/inert' { declare module.exports: any } +declare module '@hapi/joi' { declare module.exports: any } +declare module 'blakejs' { declare module.exports: any } +declare module 'buffer' { declare module.exports: any } +declare module 'chalk' { declare module.exports: any } +declare module 'dompurify' { declare module.exports: any } +declare module 'emoji-mart-vue-fast' { declare module.exports: any } +declare module 'emoji-mart-vue-fast/data/apple.json' { declare module.exports: any } +declare module 'form-data' { declare module.exports: any } +declare module 'localforage' { declare module.exports: any } +declare module 'multihashes' { declare module.exports: any } +declare module 'scrypt-async' { declare module.exports: any } +declare module 'tweetnacl' { declare module.exports: any } +declare module 'tweetnacl-util' { declare module.exports: any } +declare module 'vue' { declare module.exports: any } +declare module 'vue-clickaway' { declare module.exports: any } +declare module 'vue-router' { declare module.exports: any } +declare module 'vue-slider-component' { declare module.exports: any } +declare module 'vuelidate' { declare module.exports: any } +declare module 'vuelidate/lib/validators' { declare module.exports: any } +declare module 'vuelidate/lib/validators/maxLength' { declare module.exports: any } +declare module 'vuelidate/lib/validators/required' { declare module.exports: any } +declare module 'vuelidate/lib/validators/sameAs.js' { declare module.exports: any } +declare module 'vuex' { declare module.exports: any } +declare module 'vue2-touch-events' { declare module.exports: any } +declare module 'wicg-inert' { declare module.exports: any } +declare module 'ws' { declare module.exports: any } +declare module '@sbp/sbp' { declare module.exports: any } +declare module '@sbp/okturtles.data' { declare module.exports: any } +declare module '@sbp/okturtles.eventqueue' { declare module.exports: any } +declare module '@sbp/okturtles.events' { declare module.exports: any } + +// Only necessary because `AppStyles.vue` imports it from its script tag rather than its style tag. +declare module '@assets/style/main.scss' { declare module.exports: any } +// Other .js files. +declare module '@utils/blockies.js' { declare module.exports: Object } +declare module '~/frontend/utils/flowTyper.js' { declare module.exports: Object } diff --git a/deno-shared/domains/chelonia/GIMessage.ts b/deno-shared/domains/chelonia/GIMessage.ts new file mode 100644 index 0000000000..109e58886c --- /dev/null +++ b/deno-shared/domains/chelonia/GIMessage.ts @@ -0,0 +1,120 @@ +import { blake32Hash } from '~/shared/functions.ts' + +export class GIMessage { + static OP_CONTRACT = 'c' + static OP_ACTION_ENCRYPTED: 'ae' = 'ae' // e2e-encrypted action + static OP_ACTION_UNENCRYPTED: 'au' = 'au' // publicly readable action + static OP_KEY_ADD = 'ka' // add this key to the list of keys allowed to write to this contract, or update an existing key + static OP_KEY_DEL = 'kd' // remove this key from authorized keys + static OP_PROTOCOL_UPGRADE = 'pu' + static OP_PROP_SET = 'ps' // set a public key/value pair + static OP_PROP_DEL = 'pd' // delete a public key/value pair + + // eslint-disable-next-line camelcase + static createV1_0 ( + contractID = null, + previousHEAD = null, + op, + signatureFn = defaultSignatureFn + ) { + const message = { + version: '1.0.0', + previousHEAD, + contractID, + op, + // the nonce makes it difficult to predict message contents + // and makes it easier to prevent conflicts during development + nonce: Math.random() + } + // NOTE: the JSON strings generated here must be preserved forever. + // do not ever regenerate this message using the contructor. + // instead store it using serialize() and restore it using + // deserialize(). + const messageJSON = JSON.stringify(message) + const value = JSON.stringify({ + message: messageJSON, + sig: signatureFn(messageJSON) + }) + return new this({ + mapping: { key: blake32Hash(value), value }, + message + }) + } + + // TODO: we need signature verification upon decryption somewhere... + static deserialize (value: string): this { + if (!value) throw new Error(`deserialize bad value: ${value}`) + return new this({ + mapping: { key: blake32Hash(value), value }, + message: JSON.parse(JSON.parse(value).message) + }) + } + + constructor ({ mapping, message }: { mapping: Object, message: Object }) { + this._mapping = mapping + this._message = message + // perform basic sanity check + const [type] = this.message().op + switch (type) { + case GIMessage.OP_CONTRACT: + if (!this.isFirstMessage()) throw new Error('OP_CONTRACT: must be first message') + break + case GIMessage.OP_ACTION_ENCRYPTED: + // nothing for now + break + default: + throw new Error(`unsupported op: ${type}`) + } + } + + decryptedValue (fn?: Function): any { + if (!this._decrypted) { + this._decrypted = ( + this.opType() === GIMessage.OP_ACTION_ENCRYPTED && fn !== undefined + ? fn(this.opValue()) + : this.opValue() + ) + } + return this._decrypted + } + + message () { return this._message } + + op () { return this.message().op } + + opType () { return this.op()[0] } + + opValue () { return this.op()[1] } + + description (): string { + const type = this.opType() + let desc = `` + } + + isFirstMessage (): boolean { return !this.message().previousHEAD } + + contractID (): string { return this.message().contractID || this.hash() } + + serialize (): string { return this._mapping.value } + + hash (): string { return this._mapping.key } +} + +function defaultSignatureFn (data: string) { + return { + type: 'default', + sig: blake32Hash(data) + } +} diff --git a/deno-shared/domains/chelonia/chelonia.ts b/deno-shared/domains/chelonia/chelonia.ts new file mode 100644 index 0000000000..ce201e56df --- /dev/null +++ b/deno-shared/domains/chelonia/chelonia.ts @@ -0,0 +1,379 @@ +import sbp from '@sbp/sbp' +import '@sbp/okturtles.events' +import '@sbp/okturtles.eventqueue' +import './internals.ts' +import { CONTRACTS_MODIFIED } from './events.ts' +import { createClient, NOTIFICATION_TYPE } from '~/shared/pubsub.ts' +import { merge, cloneDeep, randomHexString, intersection, difference } from '~/shared/giLodash.ts' +// TODO: rename this to ChelMessage +import { GIMessage } from './GIMessage.ts' +import { ChelErrorUnrecoverable } from './errors.ts' + +// TODO: define ChelContractType for /defineContract + +type ChelRegParams = { + contractName: string; + data: Object; + hooks?: { + prepublishContract?: (GIMessage) => void; + prepublish?: (GIMessage) => void; + postpublish?: (GIMessage) => void; + }; + publishOptions?: { maxAttempts: number }; +} + +type ChelActionParams = { + action: string; + contractID: string; + data: Object; + hooks?: { + prepublishContract?: (GIMessage) => void; + prepublish?: (GIMessage) => void; + postpublish?: (GIMessage) => void; + }; + publishOptions?: { maxAttempts: number }; +} + +export { GIMessage } + +export const ACTION_REGEX: RegExp = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ +// ACTION_REGEX.exec('gi.contracts/group/payment/process') +// 0 => 'gi.contracts/group/payment/process' +// 1 => 'gi.contracts/group/payment/' +// 2 => 'gi.contracts/group' +// 3 => 'gi.contracts' +// 4 => 'group' +// 5 => 'payment' + +sbp('sbp/selectors/register', { + // https://www.wordnik.com/words/chelonia + // https://gitlab.okturtles.org/okturtles/group-income/-/wikis/E2E-Protocol/Framework.md#alt-names + 'chelonia/_init': function () { + this.config = { + decryptFn: JSON.parse, // override! + encryptFn: JSON.stringify, // override! + stateSelector: 'chelonia/private/state', // override to integrate with, for example, vuex + whitelisted: (action: string): boolean => !!this.whitelistedActions[action], + reactiveSet: (obj, key, value) => { obj[key] = value; return value }, // example: set to Vue.set + reactiveDel: (obj, key) => { delete obj[key] }, + skipActionProcessing: false, + skipSideEffects: false, + connectionOptions: { + maxRetries: Infinity, // See https://github.com/okTurtles/group-income/issues/1183 + reconnectOnTimeout: true, // can be enabled since we are not doing auth via web sockets + timeout: 5000 + }, + hooks: { + preHandleEvent: null, // async (message: GIMessage) => {} + postHandleEvent: null, // async (message: GIMessage) => {} + processError: null, // (e: Error, message: GIMessage) => {} + sideEffectError: null, // (e: Error, message: GIMessage) => {} + handleEventError: null, // (e: Error, message: GIMessage) => {} + syncContractError: null, // (e: Error, contractID: string) => {} + pubsubError: null // (e:Error, socket: Socket) + } + } + this.state = { + contracts: {}, // contractIDs => { type, HEAD } (contracts we've subscribed to) + pending: [] // prevents processing unexpected data from a malicious server + } + this.contracts = {} + this.whitelistedActions = {} + this.sideEffectStacks = {} // [contractID]: Array<*> + this.sideEffectStack = (contractID: string): Array<*> => { + let stack = this.sideEffectStacks[contractID] + if (!stack) { + this.sideEffectStacks[contractID] = stack = [] + } + return stack + } + }, + 'chelonia/configure': function (config: Object) { + merge(this.config, config) + // merge will strip the hooks off of config.hooks when merging from the root of the object + // because they are functions and cloneDeep doesn't clone functions + merge(this.config.hooks, config.hooks || {}) + }, + 'chelonia/connect': function (): Object { + if (!this.config.connectionURL) throw new Error('config.connectionURL missing') + if (!this.config.connectionOptions) throw new Error('config.connectionOptions missing') + if (this.pubsub) { + this.pubsub.destroy() + } + let pubsubURL = this.config.connectionURL + if (Deno.env.get('NODE_ENV') === 'development') { + // This is temporarily used in development mode to help the server improve + // its console output until we have a better solution. Do not use for auth. + pubsubURL += `?debugID=${randomHexString(6)}` + } + this.pubsub = createClient(pubsubURL, { + ...this.config.connectionOptions, + messageHandlers: { + [NOTIFICATION_TYPE.ENTRY] (msg) { + // We MUST use 'chelonia/private/in/enqueueHandleEvent' to ensure handleEvent() + // is called AFTER any currently-running calls to 'chelonia/contract/sync' + // to prevent gi.db from throwing "bad previousHEAD" errors. + // Calling via SBP also makes it simple to implement 'test/backend.js' + sbp('chelonia/private/in/enqueueHandleEvent', GIMessage.deserialize(msg.data)) + }, + [NOTIFICATION_TYPE.APP_VERSION] (msg) { + const ourVersion = Deno.env.get('GI_VERSION') + const theirVersion = msg.data + + if (ourVersion !== theirVersion) { + sbp('okTurtles.events/emit', NOTIFICATION_TYPE.APP_VERSION, theirVersion) + } + } + } + }) + if (!this.contractsModifiedListener) { + // Keep pubsub in sync (logged into the right "rooms") with 'state.contracts' + this.contractsModifiedListener = () => sbp('chelonia/pubsub/update') + sbp('okTurtles.events/on', CONTRACTS_MODIFIED, this.contractsModifiedListener) + } + return this.pubsub + }, + 'chelonia/defineContract': function (contract: Object) { + if (!ACTION_REGEX.exec(contract.name)) throw new Error(`bad contract name: ${contract.name}`) + if (!contract.metadata) contract.metadata = { validate () {}, create: () => ({}) } + if (!contract.getters) contract.getters = {} + contract.state = (contractID) => sbp(this.config.stateSelector)[contractID] + this.contracts[contract.name] = contract + sbp('sbp/selectors/register', { + // expose getters for Vuex integration and other conveniences + [`${contract.name}/getters`]: () => contract.getters, + // 2 ways to cause sideEffects to happen: by defining a sideEffect function in the + // contract, or by calling /pushSideEffect w/async SBP call. Can also do both. + [`${contract.name}/pushSideEffect`]: (contractID: string, asyncSbpCall: Array<*>) => { + this.sideEffectStack(contractID).push(asyncSbpCall) + } + }) + for (const action in contract.actions) { + contractFromAction(this.contracts, action) // ensure actions are appropriately named + this.whitelistedActions[action] = true + // TODO: automatically generate send actions here using `${action}/send` + // allow the specification of: + // - the optype (e.g. OP_ACTION_(UN)ENCRYPTED) + // - a localized error message + // - whatever keys should be passed in as well + // base it off of the design of encryptedAction() + sbp('sbp/selectors/register', { + [`${action}/process`]: (message: Object, state: Object) => { + const { meta, data, contractID } = message + // TODO: optimize so that you're creating a proxy object only when needed + const gProxy = gettersProxy(state, contract.getters) + state = state || contract.state(contractID) + contract.metadata.validate(meta, { state, ...gProxy, contractID }) + contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) + contract.actions[action].process(message, { state, ...gProxy }) + }, + [`${action}/sideEffect`]: async (message: Object, state: ?Object) => { + const sideEffects = this.sideEffectStack(message.contractID) + while (sideEffects.length > 0) { + const sideEffect = sideEffects.shift() + try { + await sbp(...sideEffect) + } catch (e) { + console.error(`[chelonia] ERROR: '${e.name}' ${e.message}, for pushed sideEffect of ${message.description()}:`, sideEffect) + this.sideEffectStacks[message.contractID] = [] // clear the side effects + throw e + } + } + if (contract.actions[action].sideEffect) { + state = state || contract.state(message.contractID) + const gProxy = gettersProxy(state, contract.getters) + await contract.actions[action].sideEffect(message, { state, ...gProxy }) + } + } + }) + } + }, + // call this manually to resubscribe/unsubscribe from contracts as needed + // if you are using a custom stateSelector and reload the state (e.g. upon login) + 'chelonia/pubsub/update': function () { + const { contracts } = sbp(this.config.stateSelector) + const client = this.pubsub + const subscribedIDs = [...client.subscriptionSet] + const currentIDs = Object.keys(contracts) + const leaveSubscribed = intersection(subscribedIDs, currentIDs) + const toUnsubscribe = difference(subscribedIDs, leaveSubscribed) + const toSubscribe = difference(currentIDs, leaveSubscribed) + // There is currently no need to tell other clients about our sub/unsubscriptions. + const dontBroadcast = true + try { + for (const contractID of toUnsubscribe) { + client.unsub(contractID, dontBroadcast) + } + for (const contractID of toSubscribe) { + client.sub(contractID, dontBroadcast) + } + } catch (e) { + console.error(`[chelonia] pubsub/update: error ${e.name}: ${e.message}`, { toUnsubscribe, toSubscribe }, e) + this.config.hooks.pubsubError?.(e, client) + } + }, + // resolves when all pending actions for these contractID(s) finish + 'chelonia/contract/wait': function (contractIDs?: string | string[]): Promise<*> { + const listOfIds = contractIDs + ? (typeof contractIDs === 'string' ? [contractIDs] : contractIDs) + : Object.keys(sbp(this.config.stateSelector).contracts) + return Promise.all(listOfIds.map(cID => { + return sbp('okTurtles.eventQueue/queueEvent', cID, ['chelonia/private/noop']) + })) + }, + // 'chelonia/contract' - selectors related to injecting remote data and monitoring contracts + // TODO: add an optional parameter to "retain" the contract (see #828) + 'chelonia/contract/sync': function (contractIDs: string | string[]): Promise<*> { + const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs + return Promise.all(listOfIds.map(contractID => { + // enqueue this invocation in a serial queue to ensure + // handleEvent does not get called on contractID while it's syncing, + // but after it's finished. This is used in tandem with + // queuing the 'chelonia/private/in/handleEvent' selector, defined below. + // This prevents handleEvent getting called with the wrong previousHEAD for an event. + return sbp('okTurtles.eventQueue/queueEvent', contractID, [ + 'chelonia/private/in/syncContract', contractID + ]).catch((err) => { + console.error(`[chelonia] failed to sync ${contractID}:`, err) + throw err // re-throw the error + }) + })) + }, + // TODO: implement 'chelonia/contract/release' (see #828) + // safer version of removeImmediately that waits to finish processing events for contractIDs + 'chelonia/contract/remove': function (contractIDs: string | string[]): Promise<*> { + const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs + return Promise.all(listOfIds.map(contractID => { + return sbp('okTurtles.eventQueue/queueEvent', contractID, [ + 'chelonia/contract/removeImmediately', contractID + ]) + })) + }, + // Warning: avoid using this unless you know what you're doing. Prefer using /remove. + 'chelonia/contract/removeImmediately': function (contractID: string) { + const state = sbp(this.config.stateSelector) + this.config.reactiveDel(state.contracts, contractID) + this.config.reactiveDel(state, contractID) + // calling this will make pubsub unsubscribe for events on `contractID` + sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, state.contracts) + }, + 'chelonia/latestContractState': async function (contractID: string) { + const events = await sbp('chelonia/private/out/eventsSince', contractID, contractID) + let state = {} + // fast-path + try { + for (const event of events) { + await sbp('chelonia/private/in/processMessage', GIMessage.deserialize(event), state) + } + return state + } catch (e) { + console.warn(`[chelonia] latestContractState(${contractID}): fast-path failed due to ${e.name}: ${e.message}`) + state = {} + } + // more error-tolerant but slower due to cloning state on each message + for (const event of events) { + const stateCopy = cloneDeep(state) + try { + await sbp('chelonia/private/in/processMessage', GIMessage.deserialize(event), state) + } catch (e) { + console.warn(`[chelonia] latestContractState: '${e.name}': ${e.message} processing:`, event) + if (e instanceof ChelErrorUnrecoverable) throw e + state = stateCopy + } + } + return state + }, + // 'chelonia/out' - selectors that send data out to the server + 'chelonia/out/registerContract': async function (params: ChelRegParams) { + const { contractName, hooks, publishOptions } = params + const contract = this.contracts[contractName] + if (!contract) throw new Error(`contract not defined: ${contractName}`) + const contractMsg = GIMessage.createV1_0(null, null, [ + GIMessage.OP_CONTRACT, + ({ + type: contractName, + keyJSON: 'TODO: add group public key here' + }: GIOpContract) + ]) + hooks && hooks.prepublishContract && hooks.prepublishContract(contractMsg) + await sbp('chelonia/private/out/publishEvent', contractMsg, publishOptions) + const msg = await sbp('chelonia/out/actionEncrypted', { + action: contractName, + contractID: contractMsg.hash(), + data: params.data, + hooks, + publishOptions + }) + return msg + }, + // all of these functions will do both the creation of the GIMessage + // and the sending of it via 'chelonia/private/out/publishEvent' + 'chelonia/out/actionEncrypted': function (params: ChelActionParams): Promise { + return outEncryptedOrUnencryptedAction.call(this, GIMessage.OP_ACTION_ENCRYPTED, params) + }, + 'chelonia/out/actionUnencrypted': function (params: ChelActionParams): Promise { + return outEncryptedOrUnencryptedAction.call(this, GIMessage.OP_ACTION_UNENCRYPTED, params) + }, + 'chelonia/out/keyAdd': async function () { + + }, + 'chelonia/out/keyDel': async function () { + + }, + 'chelonia/out/protocolUpgrade': async function () { + + }, + 'chelonia/out/propSet': async function () { + + }, + 'chelonia/out/propDel': async function () { + + } +}) + +function contractFromAction (contracts: Object, action: string): Object { + const regexResult = ACTION_REGEX.exec(action) + const contract = contracts[(regexResult && regexResult[2]) || null] + if (!contract) throw new Error(`no contract for action named: ${action}`) + return contract +} + +async function outEncryptedOrUnencryptedAction ( + opType: 'ae' | 'au', + params: ChelActionParams +) { + const { action, contractID, data, hooks, publishOptions } = params + const contract = contractFromAction(this.contracts, action) + const state = contract.state(contractID) + const previousHEAD = await sbp('chelonia/private/out/latestHash', contractID) + const meta = contract.metadata.create() + const gProxy = gettersProxy(state, contract.getters) + contract.metadata.validate(meta, { state, ...gProxy, contractID }) + contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) + const unencMessage = ({ action, data, meta }: GIOpActionUnencrypted) + const message = GIMessage.createV1_0(contractID, previousHEAD, [ + opType, + opType === GIMessage.OP_ACTION_UNENCRYPTED ? unencMessage : this.config.encryptFn(unencMessage) + ] + // TODO: add the signature function here to sign the message whether encrypted or not + ) + hooks && hooks.prepublish && hooks.prepublish(message) + await sbp('chelonia/private/out/publishEvent', message, publishOptions) + hooks && hooks.postpublish && hooks.postpublish(message) + return message +} + +// The gettersProxy is what makes Vue-like getters possible. In other words, +// we want to make sure that the getter functions that we defined in each +// contract get passed the 'state' when a getter is accessed. +// The only way to pass in the state is by creating a Proxy object that does +// that for us. This allows us to maintain compatibility with Vue.js and integrate +// the contract getters into the Vue-facing getters. +function gettersProxy (state: Object, getters: Object) { + const proxyGetters = new Proxy({}, { + get (target, prop) { + return getters[prop](state, proxyGetters) + } + }) + return { getters: proxyGetters } +} diff --git a/deno-shared/domains/chelonia/db.ts b/deno-shared/domains/chelonia/db.ts new file mode 100644 index 0000000000..ade50a0214 --- /dev/null +++ b/deno-shared/domains/chelonia/db.ts @@ -0,0 +1,87 @@ +import sbp from '@sbp/sbp' +import '@sbp/okturtles.data' +import { GIMessage } from './GIMessage.ts' +import { ChelErrorDBBadPreviousHEAD, ChelErrorDBConnection } from './errors.ts' + +const headSuffix = '-HEAD' + +// NOTE: To enable persistence of log use 'sbp/selectors/overwrite' +// to overwrite the following selectors: +sbp('sbp/selectors/unsafe', ['chelonia/db/get', 'chelonia/db/set', 'chelonia/db/delete']) +// NOTE: MAKE SURE TO CALL 'sbp/selectors/lock' after overwriting them! + +const dbPrimitiveSelectors = Deno.env.get('LIGHTWEIGHT_CLIENT') === 'true' + ? { + 'chelonia/db/get': function (key) { + const id = sbp('chelonia/db/contractIdFromLogHEAD', key) + return Promise.resolve(id ? sbp(this.config.stateSelector).contracts[id]?.HEAD : null) + }, + 'chelonia/db/set': function (key, value) { return Promise.resolve(value) }, + 'chelonia/db/delete': function () { return Promise.resolve() } + } + : { + 'chelonia/db/get': function (key: string) { + return Promise.resolve(sbp('okTurtles.data/get', key)) + }, + 'chelonia/db/set': function (key: string, value: string) { + return Promise.resolve(sbp('okTurtles.data/set', key, value)) + }, + 'chelonia/db/delete': function (key: string) { + return Promise.resolve(sbp('okTurtles.data/delete', key)) + } + } + +export default (sbp('sbp/selectors/register', { + ...dbPrimitiveSelectors, + 'chelonia/db/logHEAD': function (contractID: string): string { + return `${contractID}${headSuffix}` + }, + 'chelonia/db/contractIdFromLogHEAD': function (key: string) { + return key.endsWith(headSuffix) ? key.slice(0, -headSuffix.length) : null + }, + 'chelonia/db/latestHash': function (contractID: string) { + return sbp('chelonia/db/get', sbp('chelonia/db/logHEAD', contractID)) + }, + 'chelonia/db/getEntry': async function (hash: string) { + try { + const value: string = await sbp('chelonia/db/get', hash) + if (!value) throw new Error(`no entry for ${hash}!`) + return GIMessage.deserialize(value) + } catch (e) { + throw new ChelErrorDBConnection(`${e.name} during getEntry: ${e.message}`) + } + }, + 'chelonia/db/addEntry': async function (entry: GIMessage) { + try { + const { previousHEAD } = entry.message() + const contractID: string = entry.contractID() + if (await sbp('chelonia/db/get', entry.hash())) { + console.warn(`[chelonia.db] entry exists: ${entry.hash()}`) + return entry.hash() + } + const HEAD = await sbp('chelonia/db/latestHash', contractID) + if (!entry.isFirstMessage() && previousHEAD !== HEAD) { + console.error(`[chelonia.db] bad previousHEAD: ${previousHEAD}! Expected: ${HEAD} for contractID: ${contractID}`) + throw new ChelErrorDBBadPreviousHEAD(`bad previousHEAD: ${previousHEAD}`) + } + await sbp('chelonia/db/set', entry.hash(), entry.serialize()) + await sbp('chelonia/db/set', sbp('chelonia/db/logHEAD', contractID), entry.hash()) + console.debug(`[chelonia.db] HEAD for ${contractID} updated to:`, entry.hash()) + return entry.hash() + } catch (e) { + if (e.name.includes('ErrorDB')) { + throw e // throw the specific type of ErrorDB instance + } + throw new ChelErrorDBConnection(`${e.name} during addEntry: ${e.message}`) + } + }, + 'chelonia/db/lastEntry': async function (contractID: string) { + try { + const hash = await sbp('chelonia/db/latestHash', contractID) + if (!hash) throw new Error(`contract ${contractID} has no latest hash!`) + return sbp('chelonia/db/getEntry', hash) + } catch (e) { + throw new ChelErrorDBConnection(`${e.name} during lastEntry: ${e.message}`) + } + } +})) diff --git a/deno-shared/domains/chelonia/errors.ts b/deno-shared/domains/chelonia/errors.ts new file mode 100644 index 0000000000..e4a0d25652 --- /dev/null +++ b/deno-shared/domains/chelonia/errors.ts @@ -0,0 +1,41 @@ +export class ChelErrorDBBadPreviousHEAD extends Error { + // ugly boilerplate because JavaScript is stupid + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types + constructor (...params: any[]) { + super(...params) + // this.name = this.constructor.name + this.name = 'ChelErrorDBBadPreviousHEAD' // string literal so minifier doesn't overwrite + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor) + } + } +} +export class ChelErrorDBConnection extends Error { + constructor (...params: any[]) { + super(...params) + this.name = 'ChelErrorDBConnection' + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor) + } + } +} + +export class ChelErrorUnexpected extends Error { + constructor (...params: any[]) { + super(...params) + this.name = 'ChelErrorUnexpected' + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor) + } + } +} + +export class ChelErrorUnrecoverable extends Error { + constructor (...params: any[]) { + super(...params) + this.name = 'ChelErrorUnrecoverable' + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor) + } + } +} diff --git a/deno-shared/domains/chelonia/events.ts b/deno-shared/domains/chelonia/events.ts new file mode 100644 index 0000000000..c12268d7c4 --- /dev/null +++ b/deno-shared/domains/chelonia/events.ts @@ -0,0 +1,3 @@ +export const CONTRACT_IS_SYNCING = 'contract-is-syncing' +export const CONTRACTS_MODIFIED = 'contracts-modified' +export const EVENT_HANDLED = 'event-handled' diff --git a/deno-shared/domains/chelonia/internals.ts b/deno-shared/domains/chelonia/internals.ts new file mode 100644 index 0000000000..360c6619e0 --- /dev/null +++ b/deno-shared/domains/chelonia/internals.ts @@ -0,0 +1,332 @@ +import sbp from '@sbp/sbp' +import './db.ts' +import { GIMessage } from './GIMessage.ts' +import { b64ToStr } from '~/shared/functions.ts' +import { ChelErrorUnexpected, ChelErrorUnrecoverable } from './errors.ts' +import { CONTRACT_IS_SYNCING, CONTRACTS_MODIFIED, EVENT_HANDLED } from './events.ts' + +import { randomIntFromRange, delay, cloneDeep, debounce, pick } from '~/shared/giLodash.ts' + +function handleFetchResult (type: string) { + return function (r: Object) { + if (!r.ok) throw new Error(`${r.status}: ${r.statusText}`) + return r[type]() + } +} + +sbp('sbp/selectors/register', { + // DO NOT CALL ANY OF THESE YOURSELF! + 'chelonia/private/state': function () { + return this.state + }, + // used by, e.g. 'chelonia/contract/wait' + 'chelonia/private/noop': function () {}, + 'chelonia/private/out/publishEvent': async function (entry: GIMessage, { maxAttempts = 3 } = {}) { + const contractID = entry.contractID() + let attempt = 1 + // auto resend after short random delay + // https://github.com/okTurtles/group-income/issues/608 + while (true) { + const r = await fetch(`${this.config.connectionURL}/event`, { + method: 'POST', + body: entry.serialize(), + headers: { + 'Content-Type': 'text/plain', + 'Authorization': 'gi TODO - signature - if needed here - goes here' + } + }) + if (r.ok) { + return r.text() + } + if (r.status === 409) { + if (attempt + 1 > maxAttempts) { + console.error(`[chelonia] failed to publish ${entry.description()} after ${attempt} attempts`, entry) + throw new Error(`publishEvent: ${r.status} - ${r.statusText}. attempt ${attempt}`) + } + // create new entry + const randDelay = randomIntFromRange(0, 1500) + console.warn(`[chelonia] publish attempt ${attempt} of ${maxAttempts} failed. Waiting ${randDelay} msec before resending ${entry.description()}`) + attempt += 1 + await delay(randDelay) // wait half a second before sending it again + // if this isn't OP_CONTRACT, get latestHash, recreate and resend message + if (!entry.isFirstMessage()) { + const previousHEAD = await sbp('chelonia/private/out/latestHash', contractID) + entry = GIMessage.createV1_0(contractID, previousHEAD, entry.op()) + } + } else { + const message = (await r.json())?.message + console.error(`[chelonia] ERROR: failed to publish ${entry.description()}: ${r.status} - ${r.statusText}: ${message}`, entry) + throw new Error(`publishEvent: ${r.status} - ${r.statusText}: ${message}`) + } + } + }, + 'chelonia/private/out/latestHash': function (contractID: string) { + return fetch(`${this.config.connectionURL}/latestHash/${contractID}`, { + cache: 'no-store' + }).then(handleFetchResult('text')) + }, + // TODO: r.body is a stream.Transform, should we use a callback to process + // the events one-by-one instead of converting to giant json object? + // however, note if we do that they would be processed in reverse... + 'chelonia/private/out/eventsSince': async function (contractID: string, since: string) { + const events = await fetch(`${this.config.connectionURL}/events/${contractID}/${since}`) + .then(handleFetchResult('json')) + // console.log('eventsSince:', events) + if (Array.isArray(events)) { + return events.reverse().map(b64ToStr) + } + }, + 'chelonia/private/in/processMessage': function (message: GIMessage, state: Object) { + const [opT, opV] = message.op() + const hash = message.hash() + const contractID = message.contractID() + const config = this.config + if (!state._vm) state._vm = {} + const opFns: { [GIOpType]: (any) => void } = { + [GIMessage.OP_CONTRACT] (v: GIOpContract) { + // TODO: shouldn't each contract have its own set of authorized keys? + if (!state._vm.authorizedKeys) state._vm.authorizedKeys = [] + // TODO: we probably want to be pushing the de-JSON-ified key here + state._vm.authorizedKeys.push({ key: v.keyJSON, context: 'owner' }) + }, + [GIMessage.OP_ACTION_ENCRYPTED] (v: GIOpActionEncrypted) { + if (!config.skipActionProcessing) { + const decrypted = message.decryptedValue(config.decryptFn) + opFns[GIMessage.OP_ACTION_UNENCRYPTED](decrypted) + } + }, + [GIMessage.OP_ACTION_UNENCRYPTED] (v: GIOpActionUnencrypted) { + if (!config.skipActionProcessing) { + const { data, meta, action } = v + if (!config.whitelisted(action)) { + throw new Error(`chelonia: action not whitelisted: '${action}'`) + } + sbp(`${action}/process`, { data, meta, hash, contractID }, state) + } + }, + [GIMessage.OP_PROP_DEL]: notImplemented, + [GIMessage.OP_PROP_SET] (v: GIOpPropSet) { + if (!state._vm.props) state._vm.props = {} + state._vm.props[v.key] = v.value + }, + [GIMessage.OP_KEY_ADD] (v: GIOpKeyAdd) { + // TODO: implement this. consider creating a function so that + // we're not duplicating code in [GIMessage.OP_CONTRACT] + // if (!state._vm.authorizedKeys) state._vm.authorizedKeys = [] + // state._vm.authorizedKeys.push(v) + }, + [GIMessage.OP_KEY_DEL]: notImplemented, + [GIMessage.OP_PROTOCOL_UPGRADE]: notImplemented + } + let processOp = true + if (config.preOp) { + processOp = config.preOp(message, state) !== false && processOp + } + if (config[`preOp_${opT}`]) { + processOp = config[`preOp_${opT}`](message, state) !== false && processOp + } + if (processOp && !config.skipProcessing) { + opFns[opT](opV) + config.postOp && config.postOp(message, state) + config[`postOp_${opT}`] && config[`postOp_${opT}`](message, state) + } + }, + 'chelonia/private/in/enqueueHandleEvent': function (event: GIMessage) { + // make sure handleEvent is called AFTER any currently-running invocations + // to 'chelonia/contract/sync', to prevent gi.db from throwing + // "bad previousHEAD" errors + return sbp('okTurtles.eventQueue/queueEvent', event.contractID(), [ + 'chelonia/private/in/handleEvent', event + ]) + }, + 'chelonia/private/in/syncContract': async function (contractID: string) { + const state = sbp(this.config.stateSelector) + const latest = await sbp('chelonia/private/out/latestHash', contractID) + console.debug(`syncContract: ${contractID} latestHash is: ${latest}`) + // there is a chance two users are logged in to the same machine and must check their contracts before syncing + let recent + if (state.contracts[contractID]) { + recent = state.contracts[contractID].HEAD + } else { + // we're syncing a contract for the first time, make sure to add to pending + // so that handleEvents knows to expect events from this contract + if (!state.contracts[contractID] && !state.pending.includes(contractID)) { + state.pending.push(contractID) + } + } + sbp('okTurtles.events/emit', CONTRACT_IS_SYNCING, contractID, true) + try { + if (latest !== recent) { + console.debug(`[chelonia] Synchronizing Contract ${contractID}: our recent was ${recent || 'undefined'} but the latest is ${latest}`) + // TODO: fetch events from localStorage instead of server if we have them + const events = await sbp('chelonia/private/out/eventsSince', contractID, recent || contractID) + // remove the first element in cases where we are not getting the contract for the first time + state.contracts[contractID] && events.shift() + for (let i = 0; i < events.length; i++) { + // this must be called directly, instead of via enqueueHandleEvent + await sbp('chelonia/private/in/handleEvent', GIMessage.deserialize(events[i])) + } + } else { + console.debug(`[chelonia] contract ${contractID} was already synchronized`) + } + sbp('okTurtles.events/emit', CONTRACT_IS_SYNCING, contractID, false) + } catch (e) { + console.error(`[chelonia] syncContract error: ${e.message}`, e) + sbp('okTurtles.events/emit', CONTRACT_IS_SYNCING, contractID, false) + this.config.hooks.syncContractError?.(e, contractID) + throw e + } + }, + 'chelonia/private/in/handleEvent': async function (message: GIMessage) { + const state = sbp(this.config.stateSelector) + const contractID = message.contractID() + const hash = message.hash() + const { preHandleEvent, postHandleEvent, handleEventError } = this.config.hooks + let processingErrored = false + // Errors in mutations result in ignored messages + // Errors in side effects result in dropped messages to be reprocessed + try { + preHandleEvent && await preHandleEvent(message) + // verify we're expecting to hear from this contract + if (!state.pending.includes(contractID) && !state.contracts[contractID]) { + console.warn(`[chelonia] WARN: ignoring unexpected event ${message.description()}:`, message.serialize()) + throw new ChelErrorUnexpected() + } + // the order the following actions are done is critically important! + // first we make sure we save this message to the db + // if an exception is thrown here we do not need to revert the state + // because nothing has been processed yet + const proceed = await handleEvent.addMessageToDB(message) + if (proceed === false) return + + const contractStateCopy = cloneDeep(state[contractID] || null) + const stateCopy = cloneDeep(pick(state, ['pending', 'contracts'])) + // process the mutation on the state + // IMPORTANT: even though we 'await' processMutation, everything in your + // contract's 'process' function must be synchronous! The only + // reason we 'await' here is to dynamically load any new contract + // source / definitions specified by the GIMessage + try { + await handleEvent.processMutation.call(this, message, state) + } catch (e) { + console.error(`[chelonia] ERROR '${e.name}' in processMutation for ${message.description()}: ${e.message}`, e, message.serialize()) + // we revert any changes to the contract state that occurred, ignoring this mutation + handleEvent.revertProcess.call(this, { message, state, contractID, contractStateCopy }) + processingErrored = true + this.config.hooks.processError?.(e, message) + // special error that prevents the head from being updated, effectively killing the contract + if (e.name === 'ChelErrorUnrecoverable') throw e + } + // whether or not there was an exception, we proceed ahead with updating the head + // you can prevent this by throwing an exception in the processError hook + state.contracts[contractID].HEAD = hash + // process any side-effects (these must never result in any mutation to the contract state!) + if (!processingErrored) { + try { + if (!this.config.skipActionProcessing && !this.config.skipSideEffects) { + await handleEvent.processSideEffects.call(this, message) + } + postHandleEvent && await postHandleEvent(message) + sbp('okTurtles.events/emit', hash, contractID, message) + sbp('okTurtles.events/emit', EVENT_HANDLED, contractID, message) + } catch (e) { + console.error(`[chelonia] ERROR '${e.name}' in side-effects for ${message.description()}: ${e.message}`, e, message.serialize()) + // revert everything + handleEvent.revertSideEffect.call(this, { message, state, contractID, contractStateCopy, stateCopy }) + this.config.hooks.sideEffectError?.(e, message) + throw e // rethrow to prevent the contract sync from going forward + } + } + } catch (e) { + console.error(`[chelonia] ERROR in handleEvent: ${e.message}`, e) + handleEventError?.(e, message) + throw e + } + } +}) + +const eventsToReinjest = [] +const reprocessDebounced = debounce((contractID) => sbp('chelonia/contract/sync', contractID), 1000) + +const handleEvent = { + async addMessageToDB (message: GIMessage) { + const contractID = message.contractID() + const hash = message.hash() + try { + await sbp('chelonia/db/addEntry', message) + const reprocessIdx = eventsToReinjest.indexOf(hash) + if (reprocessIdx !== -1) { + console.warn(`[chelonia] WARN: successfully reinjested ${message.description()}`) + eventsToReinjest.splice(reprocessIdx, 1) + } + } catch (e) { + if (e.name === 'ChelErrorDBBadPreviousHEAD') { + // sometimes we simply miss messages, it's not clear why, but it happens + // in rare cases. So we attempt to re-sync this contract once + if (eventsToReinjest.length > 100) { + throw new ChelErrorUnrecoverable('more than 100 different bad previousHEAD errors') + } + if (!eventsToReinjest.includes(hash)) { + console.warn(`[chelonia] WARN bad previousHEAD for ${message.description()}, will attempt to re-sync contract to reinjest message`) + eventsToReinjest.push(hash) + reprocessDebounced(contractID) + return false // ignore the error for now + } else { + console.error(`[chelonia] ERROR already attempted to reinjest ${message.description()}, will not attempt again!`) + } + } + throw e + } + }, + async processMutation (message: GIMessage, state: Object) { + const contractID = message.contractID() + if (message.isFirstMessage()) { + // Flow doesn't understand that a first message must be a contract, + // so we have to help it a bit in order to acces the 'type' property. + const { type } = ((message.opValue(): any): GIOpContract) + if (!state[contractID]) { + console.debug(`contract ${type} registered for ${contractID}`) + this.config.reactiveSet(state, contractID, {}) + this.config.reactiveSet(state.contracts, contractID, { type, HEAD: contractID }) + } + // we've successfully received it back, so remove it from expectation pending + const index = state.pending.indexOf(contractID) + index !== -1 && state.pending.splice(index, 1) + sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, state.contracts) + } + await Promise.resolve() // TODO: load any unloaded contract code + sbp('chelonia/private/in/processMessage', message, state[contractID]) + }, + async processSideEffects (message: GIMessage) { + if ([GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_ACTION_UNENCRYPTED].includes(message.opType())) { + const contractID = message.contractID() + const hash = message.hash() + const { action, data, meta } = message.decryptedValue() + const mutation = { data, meta, hash, contractID } + await sbp(`${action}/sideEffect`, mutation) + } + }, + revertProcess ({ message, state, contractID, contractStateCopy }) { + console.warn(`[chelonia] reverting mutation ${message.description()}: ${message.serialize()}. Any side effects will be skipped!`) + if (!contractStateCopy) { + console.warn(`[chelonia] mutation reversion on very first message for contract ${contractID}! Your contract may be too damaged to be useful and should be redeployed with bugfixes.`) + contractStateCopy = {} + } + this.config.reactiveSet(state, contractID, contractStateCopy) + }, + revertSideEffect ({ message, state, contractID, contractStateCopy, stateCopy }) { + console.warn(`[chelonia] reverting entire state because failed sideEffect for ${message.description()}: ${message.serialize()}`) + if (!contractStateCopy) { + this.config.reactiveDel(state, contractID) + } else { + this.config.reactiveSet(state, contractID, contractStateCopy) + } + state.contracts = stateCopy.contracts + state.pending = stateCopy.pending + sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, state.contracts) + } +} + +const notImplemented = (v) => { + throw new Error(`chelonia: action not implemented to handle: ${JSON.stringify(v)}.`) +} diff --git a/deno-shared/functions.ts b/deno-shared/functions.ts new file mode 100644 index 0000000000..e5ee12a631 --- /dev/null +++ b/deno-shared/functions.ts @@ -0,0 +1,45 @@ +import multihash from 'multihashes' +import nacl from 'tweetnacl' +import blake from 'blakejs' + +import { Buffer } from 'buffer' + +export function blake32Hash (data: string | Buffer | Uint8Array): string { + // TODO: for node/electron, switch to: https://github.com/ludios/node-blake2 + const uint8array = blake.blake2b(data, null, 32) + // TODO: if we switch to webpack we may need: https://github.com/feross/buffer + // https://github.com/feross/typedarray-to-buffer + const buf = Buffer.from(uint8array.buffer) + return multihash.toB58String(multihash.encode(buf, 'blake2b-32', 32)) +} + +// NOTE: to preserve consistency across browser and node, we use the Buffer +// class. We could use btoa and atob in web browsers (functions that +// are unavailable on Node.js), but they do not support Unicode, +// and you have to jump through some hoops to get it to work: +// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa#Unicode_strings +// These hoops might result in inconsistencies between Node.js and the frontend. +export const b64ToBuf = (b64: string): Buffer => Buffer.from(b64, 'base64') +export const b64ToStr = (b64: string): string => b64ToBuf(b64).toString('utf8') +export const bufToB64 = (buf: Buffer): string => Buffer.from(buf).toString('base64') +export const strToBuf = (str: string): Buffer => Buffer.from(str, 'utf8') +export const strToB64 = (str: string): string => strToBuf(str).toString('base64') +export const bytesToB64 = (ary: Uint8Array): string => Buffer.from(ary).toString('base64') + +export function sign ( + { publicKey, secretKey }: {publicKey: string, secretKey: string}, + msg: string = 'hello!', + futz: string = '' +): string { + return strToB64(JSON.stringify({ + msg: msg + futz, + key: publicKey, + sig: bytesToB64(nacl.sign.detached(strToBuf(msg), b64ToBuf(secretKey))) + })) +} + +export function verify ( + msg: string, key: string, sig: string +): any { + return nacl.sign.detached.verify(strToBuf(msg), b64ToBuf(sig), b64ToBuf(key)) +} diff --git a/deno-shared/giLodash.ts b/deno-shared/giLodash.ts new file mode 100644 index 0000000000..aa255a0374 --- /dev/null +++ b/deno-shared/giLodash.ts @@ -0,0 +1,214 @@ +// manually implemented lodash functions are better than even: +// https://github.com/lodash/babel-plugin-lodash +// additional tiny versions of lodash functions are available in VueScript2 + +export function mapValues (obj: Object, fn: Function, o: Object = {}): any { + for (const key in obj) { o[key] = fn(obj[key]) } + return o +} + +export function mapObject (obj: Object, fn: Function): {[any]: any} { + return Object.fromEntries(Object.entries(obj).map(fn)) +} + +export function pick (o: Object, props: string[]): Object { + const x = {} + for (const k of props) { x[k] = o[k] } + return x +} + +export function pickWhere (o: Object, where: Function): Object { + const x = {} + for (const k in o) { + if (where(o[k])) { x[k] = o[k] } + } + return x +} + +export function choose (array: Array<*>, indices: Array): Array<*> { + const x = [] + for (const idx of indices) { x.push(array[idx]) } + return x +} + +export function omit (o: Object, props: string[]): {...} { + const x = {} + for (const k in o) { + if (!props.includes(k)) { + x[k] = o[k] + } + } + return x +} + +export function cloneDeep (obj: Object): any { + return JSON.parse(JSON.stringify(obj)) +} + +function isMergeableObject (val) { + const nonNullObject = val && typeof val === 'object' + // $FlowFixMe + return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]' +} + +export function merge (obj: Object, src: Object): any { + for (const key in src) { + const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined + if (clone && isMergeableObject(obj[key])) { + merge(obj[key], clone) + continue + } + obj[key] = clone || src[key] + } + return obj +} + +export function delay (msec: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(resolve, msec) + }) +} + +export function randomBytes (length: number): Uint8Array { + // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019 + return crypto.getRandomValues(new Uint8Array(length)) +} + +export function randomHexString (length: number): string { + return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('') +} + +export function randomIntFromRange (min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1) + min) +} + +export function randomFromArray (arr: any[]): any { + return arr[Math.floor(Math.random() * arr.length)] +} + +export function flatten (arr: Array<*>): Array { + let flat: Array<*> = [] + for (let i = 0; i < arr.length; i++) { + if (Array.isArray(arr[i])) { + flat = flat.concat(arr[i]) + } else { + flat.push(arr[i]) + } + } + return flat +} + +export function zip (): any[] { + // $FlowFixMe + const arr = Array.prototype.slice.call(arguments) + const zipped = [] + let max = 0 + arr.forEach((current) => (max = Math.max(max, current.length))) + for (const current of arr) { + for (let i = 0; i < max; i++) { + zipped[i] = zipped[i] || [] + zipped[i].push(current[i]) + } + } + return zipped +} + +export function uniq (array: any[]): any[] { + return Array.from(new Set(array)) +} + +export function union (...arrays: any[][]): any[] { + // $FlowFixMe + return uniq([].concat.apply([], arrays)) +} + +export function intersection (a1: any[], ...arrays: any[][]): any[] { + return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0)) +} + +export function difference (a1: any[], ...arrays: any[][]): any[] { + // $FlowFixMe + const a2 = [].concat.apply([], arrays) + return a1.filter(v => a2.indexOf(v) === -1) +} + +export function deepEqualJSONType (a: any, b: any): boolean { + if (a === b) return true + if (a === null || b === null || typeof (a) !== typeof (b)) return false + if (typeof a !== 'object') return a === b + if (Array.isArray(a)) { + if (a.length !== b.length) return false + } else if (a.constructor.name !== 'Object') { + throw new Error(`not JSON type: ${a}`) + } + for (const key in a) { + if (!deepEqualJSONType(a[key], b[key])) return false + } + return true +} + +/** + * Modified version of: https://github.com/component/debounce/blob/master/index.js + * Returns a function, that, as long as it continues to be invoked, will not + * be triggered. The function will be called after it stops being called for + * N milliseconds. If `immediate` is passed, trigger the function on the + * leading edge, instead of the trailing. The function also has a property 'clear' + * that is a function which will clear the timer to prevent previously scheduled executions. + * + * @source underscore.js + * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/ + * @param {Function} function to wrap + * @param {Number} timeout in ms (`100`) + * @param {Boolean} whether to execute at the beginning (`false`) + * @api public + */ +export function debounce (func: Function, wait: number, immediate: ?boolean): Function { + let timeout, args, context, timestamp, result + if (wait == null) wait = 100 + + function later () { + const last = Date.now() - timestamp + + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last) + } else { + timeout = null + if (!immediate) { + result = func.apply(context, args) + context = args = null + } + } + } + + const debounced = function () { + context = this + args = arguments + timestamp = Date.now() + const callNow = immediate && !timeout + if (!timeout) timeout = setTimeout(later, wait) + if (callNow) { + result = func.apply(context, args) + context = args = null + } + + return result + } + + debounced.clear = function () { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + } + + debounced.flush = function () { + if (timeout) { + result = func.apply(context, args) + context = args = null + clearTimeout(timeout) + timeout = null + } + } + + return debounced +} diff --git a/deno-shared/pubsub.test.ts b/deno-shared/pubsub.test.ts new file mode 100644 index 0000000000..2cf442b6a0 --- /dev/null +++ b/deno-shared/pubsub.test.ts @@ -0,0 +1,52 @@ +/* eslint-env mocha */ +'use strict' + +import { createClient } from './pubsub.js' + +const should = require('should') // eslint-disable-line + +const client = createClient('ws://localhost:8080', { + manual: true, + reconnectOnDisconnection: false, + reconnectOnOnline: false, + reconnectOnTimeout: false +}) +const { + maxReconnectionDelay, + minReconnectionDelay +} = client.options + +const createRandomDelays = (number) => { + return [...new Array(number)].map((_, i) => { + client.failedConnectionAttempts = i + return client.getNextRandomDelay() + }) +} +const delays1 = createRandomDelays(10) +const delays2 = createRandomDelays(10) + +describe('Test getNextRandomDelay()', function () { + it('every delay should be longer than the previous one', function () { + // In other words, the delays should be sorted in ascending numerical order. + should(delays1).deepEqual([...delays1].sort((a, b) => a - b)) + should(delays2).deepEqual([...delays2].sort((a, b) => a - b)) + }) + + it('no delay should be shorter than the minimal reconnection delay', function () { + delays1.forEach((delay) => { + should(delay).be.greaterThanOrEqual(minReconnectionDelay) + }) + delays2.forEach((delay) => { + should(delay).be.greaterThanOrEqual(minReconnectionDelay) + }) + }) + + it('no delay should be longer than the maximal reconnection delay', function () { + delays1.forEach((delay) => { + should(delay).be.lessThanOrEqual(maxReconnectionDelay) + }) + delays2.forEach((delay) => { + should(delay).be.lessThanOrEqual(maxReconnectionDelay) + }) + }) +}) diff --git a/deno-shared/pubsub.ts b/deno-shared/pubsub.ts new file mode 100644 index 0000000000..e17a52eae6 --- /dev/null +++ b/deno-shared/pubsub.ts @@ -0,0 +1,660 @@ +import sbp from '@sbp/sbp' +import '@sbp/okturtles.events' + +// ====== Event name constants ====== // + +export const PUBSUB_ERROR = 'pubsub-error' +export const PUBSUB_RECONNECTION_ATTEMPT = 'pubsub-reconnection-attempt' +export const PUBSUB_RECONNECTION_FAILED = 'pubsub-reconnection-failed' +export const PUBSUB_RECONNECTION_SCHEDULED = 'pubsub-reconnection-scheduled' +export const PUBSUB_RECONNECTION_SUCCEEDED = 'pubsub-reconnection-succeeded' + +// ====== Types ====== // + +/* + * Flowtype usage notes: + * + * - The '+' prefix indicates properties that should not be re-assigned or + * deleted after their initialization. + * + * - 'TimeoutID' is an opaque type declared in Flow's core definition file, + * used as the return type of the core setTimeout() function. + */ + +type Message = { + [key: string]: JSONType, + type: string +} + +type PubSubClient = { + connectionTimeoutID: TimeoutID | void, + customEventHandlers: Object, + failedConnectionAttempts: number, + isLocal: boolean, + isNew: boolean, + listeners: Object, + messageHandlers: Object, + nextConnectionAttemptDelayID: TimeoutID | void, + options: Object, + pendingSubscriptionSet: Set, + pendingSyncSet: Set, + pendingUnsubscriptionSet: Set, + pingTimeoutID: TimeoutID | void, + shouldReconnect: boolean, + socket: WebSocket | null, + subscriptionSet: Set, + url: string, + // Methods + clearAllTimers(): void, + connect(): void, + destroy(): void, + pub(contractID: string, data: JSONType): void, + scheduleConnectionAttempt(): void, + sub(contractID: string): void, + unsub(contractID: string): void +} + +type SubMessage = { + [key: string]: JSONType, + type: 'sub', + contractID: string, + dontBroadcast: boolean +} + +type UnsubMessage = { + [key: string]: JSONType, + type: 'unsub', + contractID: string, + dontBroadcast: boolean +} + +// ====== Enums ====== // + +export const NOTIFICATION_TYPE = Object.freeze({ + ENTRY: 'entry', + APP_VERSION: 'app_version', + PING: 'ping', + PONG: 'pong', + PUB: 'pub', + SUB: 'sub', + UNSUB: 'unsub' +}) + +export const REQUEST_TYPE = Object.freeze({ + PUB: 'pub', + SUB: 'sub', + UNSUB: 'unsub' +}) + +export const RESPONSE_TYPE = Object.freeze({ + ERROR: 'error', + SUCCESS: 'success' +}) + +export type NotificationTypeEnum = $Values +export type RequestTypeEnum = $Values +export type ResponseTypeEnum = $Values + +// ====== API ====== // + +/** + * Creates a pubsub client instance. + * + * @param {string} url - A WebSocket URL to connect to. + * @param {Object?} options + * {object?} handlers - Custom handlers for WebSocket events. + * {boolean?} logPingMessages - Whether to log received pings. + * {boolean?} manual - Whether the factory should call 'connect()' automatically. + * Also named 'autoConnect' or 'startClosed' in other libraries. + * {object?} messageHandlers - Custom handlers for different message types. + * {number?} pingTimeout=45_000 - How long to wait for the server to send a ping, in milliseconds. + * {boolean?} reconnectOnDisconnection=true - Whether to reconnect after a server-side disconnection. + * {boolean?} reconnectOnOnline=true - Whether to reconnect after coming back online. + * {boolean?} reconnectOnTimeout=false - Whether to reconnect after a connection timeout. + * {number?} timeout=5_000 - Connection timeout duration in milliseconds. + * @returns {PubSubClient} + */ +export function createClient (url: string, options?: Object = {}): PubSubClient { + const client: PubSubClient = { + customEventHandlers: options.handlers || {}, + // The current number of connection attempts that failed. + // Reset to 0 upon successful connection. + // Used to compute how long to wait before the next reconnection attempt. + failedConnectionAttempts: 0, + isLocal: /\/\/(localhost|127\.0\.0\.1)([:?/]|$)/.test(url), + // True if this client has never been connected yet. + isNew: true, + listeners: Object.create(null), + messageHandlers: { ...defaultMessageHandlers, ...options.messageHandlers }, + nextConnectionAttemptDelayID: undefined, + options: { ...defaultOptions, ...options }, + // Requested subscriptions for which we didn't receive a response yet. + pendingSubscriptionSet: new Set(), + pendingSyncSet: new Set(), + pendingUnsubscriptionSet: new Set(), + pingTimeoutID: undefined, + shouldReconnect: true, + // The underlying WebSocket object. + // A new one is necessary for every connection or reconnection attempt. + socket: null, + subscriptionSet: new Set(), + connectionTimeoutID: undefined, + url: url.replace(/^http/, 'ws'), + ...publicMethods + } + // Create and save references to reusable event listeners. + // Every time a new underlying WebSocket object will be created for this + // client instance, these event listeners will be detached from the older + // socket then attached to the new one, hereby avoiding both unnecessary + // allocations and garbage collections of a bunch of functions every time. + // Another benefit is the ability to patch the client protocol at runtime by + // updating the client's custom event handler map. + for (const name of Object.keys(defaultClientEventHandlers)) { + client.listeners[name] = (event) => { + try { + // Use `.call()` to pass the client via the 'this' binding. + defaultClientEventHandlers[name]?.call(client, event) + client.customEventHandlers[name]?.call(client, event) + } catch (error) { + // Do not throw any error but emit an `error` event instead. + sbp('okTurtles.events/emit', PUBSUB_ERROR, client, error.message) + } + } + } + // Add global event listeners before the first connection. + if (typeof window === 'object') { + for (const name of globalEventNames) { + window.addEventListener(name, client.listeners[name]) + } + } + if (!client.options.manual) { + client.connect() + } + return client +} + +export function createMessage (type: string, data: JSONType): string { + return JSON.stringify({ type, data }) +} + +export function createNotification (type: string, data: JSONType): string { + return JSON.stringify({ type, data }) +} + +export function createRequest (type: RequestTypeEnum, data: JSONObject, dontBroadcast: boolean = false): string { + // Had to use Object.assign() instead of object spreading to make Flow happy. + return JSON.stringify(Object.assign({ type, dontBroadcast }, data)) +} + +// These handlers receive the PubSubClient instance through the `this` binding. +const defaultClientEventHandlers = { + // Emitted when the connection is closed. + close (event: CloseEvent) { + const client = this + + console.debug('[pubsub] Event: close', event.code, event.reason) + client.failedConnectionAttempts++ + + if (client.socket) { + // Remove event listeners to avoid memory leaks. + for (const name of socketEventNames) { + client.socket.removeEventListener(name, client.listeners[name]) + } + } + client.socket = null + client.clearAllTimers() + + // See "Status Codes" https://tools.ietf.org/html/rfc6455#section-7.4 + switch (event.code) { + // TODO: verify that this list of codes is correct. + case 1000: case 1002: case 1003: case 1007: case 1008: { + client.shouldReconnect = false + break + } + default: break + } + // If we should reconnect then consider our current subscriptions as pending again, + // waiting to be restored upon reconnection. + if (client.shouldReconnect) { + client.subscriptionSet.forEach((contractID) => { + // Skip contracts from which we had to unsubscribe anyway. + if (!client.pendingUnsubscriptionSet.has(contractID)) { + client.pendingSubscriptionSet.add(contractID) + } + }) + } + // We are no longer subscribed to any contracts since we are now disconnected. + client.subscriptionSet.clear() + client.pendingUnsubscriptionSet.clear() + + if (client.shouldReconnect && client.options.reconnectOnDisconnection) { + if (client.failedConnectionAttempts > client.options.maxRetries) { + sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_FAILED, client) + } else { + // If we are definetely offline then do not try to reconnect now, + // unless the server is local. + if (!isDefinetelyOffline() || client.isLocal) { + client.scheduleConnectionAttempt() + } + } + } + }, + + // Emitted when an error has occured. + // The socket will be closed automatically by the engine if necessary. + error (event: Event) { + const client = this + // Not all error events should be logged with console.error, for example every + // failed connection attempt generates one such event. + console.warn('[pubsub] Event: error', event) + clearTimeout(client.pingTimeoutID) + }, + + // Emitted when a message is received. + // The connection will be terminated if the message is malformed or has an + // unexpected data type (e.g. binary instead of text). + message (event: MessageEvent) { + const client = this + const { data } = event + + if (typeof data !== 'string') { + sbp('okTurtles.events/emit', PUBSUB_ERROR, client, { + message: `Wrong data type: ${typeof data}` + }) + return client.destroy() + } + let msg: Message = { type: '' } + + try { + msg = messageParser(data) + } catch (error) { + sbp('okTurtles.events/emit', PUBSUB_ERROR, client, { + message: `Malformed message: ${error.message}` + }) + return client.destroy() + } + const handler = client.messageHandlers[msg.type] + + if (handler) { + handler.call(client, msg) + } else { + throw new Error(`Unhandled message type: ${msg.type}`) + } + }, + + offline (event: Event) { + console.info('[pubsub] Event: offline') + const client = this + + client.clearAllTimers() + // Reset the connection attempt counter so that we'll start a new + // reconnection loop when we are back online. + client.failedConnectionAttempts = 0 + client.socket?.close() + }, + + online (event: Event) { + console.info('[pubsub] Event: online') + const client = this + + if (client.options.reconnectOnOnline && client.shouldReconnect) { + if (!client.socket) { + client.failedConnectionAttempts = 0 + client.scheduleConnectionAttempt() + } + } + }, + + // Emitted when the connection is established. + open (event: Event) { + console.debug('[pubsub] Event: open') + const client = this + const { options } = this + + client.clearAllTimers() + sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_SUCCEEDED, client) + + // Set it to -1 so that it becomes 0 on the next `close` event. + client.failedConnectionAttempts = -1 + client.isNew = false + // Setup a ping timeout if required. + // It will close the connection if we don't get any message from the server. + if (options.pingTimeout > 0 && options.pingTimeout < Infinity) { + client.pingTimeoutID = setTimeout(() => { + client.socket?.close() + }, options.pingTimeout) + } + // We only need to handle contract resynchronization here when reconnecting. + // Not on initial connection, since the login code already does it. + if (!client.isNew) { + client.pendingSyncSet = new Set(client.pendingSubscriptionSet) + } + // Send any pending subscription request. + client.pendingSubscriptionSet.forEach((contractID) => { + client.socket?.send(createRequest(REQUEST_TYPE.SUB, { contractID }, true)) + }) + // There should be no pending unsubscription since we just got connected. + }, + + 'reconnection-attempt' (event: CustomEvent) { + console.info('[pubsub] Trying to reconnect...') + }, + + 'reconnection-succeeded' (event: CustomEvent) { + console.info('[pubsub] Connection re-established') + }, + + 'reconnection-failed' (event: CustomEvent) { + console.warn('[pubsub] Reconnection failed') + const client = this + + client.destroy() + }, + + 'reconnection-scheduled' (event: CustomEvent) { + const { delay, nth } = event.detail + console.info(`[pubsub] Scheduled connection attempt ${nth} in ~${delay} ms`) + } +} + +// These handlers receive the PubSubClient instance through the `this` binding. +const defaultMessageHandlers = { + [NOTIFICATION_TYPE.ENTRY] (msg) { + console.debug('[pubsub] Received ENTRY:', msg) + }, + + [NOTIFICATION_TYPE.PING] ({ data }) { + const client = this + + if (client.options.logPingMessages) { + console.debug(`[pubsub] Ping received in ${Date.now() - Number(data)} ms`) + } + // Reply with a pong message using the same data. + client.socket?.send(createMessage(NOTIFICATION_TYPE.PONG, data)) + // Refresh the ping timer, waiting for the next ping. + clearTimeout(client.pingTimeoutID) + client.pingTimeoutID = setTimeout(() => { + client.socket?.close() + }, client.options.pingTimeout) + }, + + // PUB can be used to send ephemeral messages outside of any contract log. + [NOTIFICATION_TYPE.PUB] (msg) { + console.debug(`[pubsub] Ignoring ${msg.type} message:`, msg.data) + }, + + [NOTIFICATION_TYPE.SUB] (msg) { + console.debug(`[pubsub] Ignoring ${msg.type} message:`, msg.data) + }, + + [NOTIFICATION_TYPE.UNSUB] (msg) { + console.debug(`[pubsub] Ignoring ${msg.type} message:`, msg.data) + }, + + [RESPONSE_TYPE.ERROR] ({ data: { type, contractID } }) { + console.warn(`[pubsub] Received ERROR response for ${type} request to ${contractID}`) + const client = this + + switch (type) { + case REQUEST_TYPE.SUB: { + console.warn(`[pubsub] Could not subscribe to ${contractID}`) + client.pendingSubscriptionSet.delete(contractID) + client.pendingSyncSet.delete(contractID) + break + } + case REQUEST_TYPE.UNSUB: { + console.warn(`[pubsub] Could not unsubscribe from ${contractID}`) + client.pendingUnsubscriptionSet.delete(contractID) + break + } + default: { + console.error(`[pubsub] Malformed response: invalid request type ${type}`) + } + } + }, + + [RESPONSE_TYPE.SUCCESS] ({ data: { type, contractID } }) { + const client = this + + switch (type) { + case REQUEST_TYPE.SUB: { + console.debug(`[pubsub] Subscribed to ${contractID}`) + client.pendingSubscriptionSet.delete(contractID) + client.subscriptionSet.add(contractID) + if (client.pendingSyncSet.has(contractID)) { + sbp('chelonia/contract/sync', contractID) + client.pendingSyncSet.delete(contractID) + } + break + } + case REQUEST_TYPE.UNSUB: { + console.debug(`[pubsub] Unsubscribed from ${contractID}`) + client.pendingUnsubscriptionSet.delete(contractID) + client.subscriptionSet.delete(contractID) + break + } + default: { + console.error(`[pubsub] Malformed response: invalid request type ${type}`) + } + } + } +} + +// TODO: verify these are good defaults +const defaultOptions = { + logPingMessages: Deno.env.get('NODE_ENV') !== 'production' && !Deno.env.get('CI'), + pingTimeout: 45000, + maxReconnectionDelay: 60000, + maxRetries: 10, + minReconnectionDelay: 500, + reconnectOnDisconnection: true, + reconnectOnOnline: true, + // Defaults to false to avoid reconnection attempts in case the server doesn't + // respond because of a failed authentication. + reconnectOnTimeout: false, + reconnectionDelayGrowFactor: 2, + timeout: 5000 +} + +const globalEventNames = ['offline', 'online'] +const socketEventNames = ['close', 'error', 'message', 'open'] + +// `navigator.onLine` can give confusing false positives when `true`, +// so we'll define `isDefinetelyOffline()` rather than `isOnline()` or `isOffline()`. +// See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine +const isDefinetelyOffline = () => typeof navigator === 'object' && navigator.onLine === false + +// Parses and validates a received message. +export const messageParser = (data: string): Message => { + const msg = JSON.parse(data) + + if (typeof msg !== 'object' || msg === null) { + throw new TypeError('Message is null or not an object') + } + const { type } = msg + + if (typeof type !== 'string' || type === '') { + throw new TypeError('Message type must be a non-empty string') + } + return msg +} + +const publicMethods = { + clearAllTimers () { + const client = this + + clearTimeout(client.connectionTimeoutID) + clearTimeout(client.nextConnectionAttemptDelayID) + clearTimeout(client.pingTimeoutID) + client.connectionTimeoutID = undefined + client.nextConnectionAttemptDelayID = undefined + client.pingTimeoutID = undefined + }, + + // Performs a connection or reconnection attempt. + connect () { + const client = this + + if (client.socket !== null) { + throw new Error('connect() can only be called if there is no current socket.') + } + if (client.nextConnectionAttemptDelayID) { + throw new Error('connect() must not be called during a reconnection delay.') + } + if (!client.shouldReconnect) { + throw new Error('connect() should no longer be called on this instance.') + } + client.socket = new WebSocket(client.url) + + if (client.options.timeout) { + client.connectionTimeoutID = setTimeout(() => { + client.connectionTimeoutID = undefined + client.socket?.close(4000, 'timeout') + }, client.options.timeout) + } + // Attach WebSocket event listeners. + for (const name of socketEventNames) { + client.socket.addEventListener(name, client.listeners[name]) + } + }, + + /** + * Immediately close the socket, stop listening for events and clear any cache. + * + * This method is used in unit tests. + * - In particular, no 'close' event handler will be called. + * - Any incoming or outgoing buffered data will be discarded. + * - Any pending messages will be discarded. + */ + destroy () { + const client = this + + client.clearAllTimers() + // Update property values. + // Note: do not clear 'client.options'. + client.pendingSubscriptionSet.clear() + client.pendingUnsubscriptionSet.clear() + client.subscriptionSet.clear() + // Remove global event listeners. + if (typeof window === 'object') { + for (const name of globalEventNames) { + window.removeEventListener(name, client.listeners[name]) + } + } + // Remove WebSocket event listeners. + if (client.socket) { + for (const name of socketEventNames) { + client.socket.removeEventListener(name, client.listeners[name]) + } + client.socket.close() + } + client.listeners = {} + client.socket = null + client.shouldReconnect = false + }, + + getNextRandomDelay (): number { + const client = this + + const { + maxReconnectionDelay, + minReconnectionDelay, + reconnectionDelayGrowFactor + } = client.options + + const minDelay = minReconnectionDelay * reconnectionDelayGrowFactor ** client.failedConnectionAttempts + const maxDelay = minDelay * reconnectionDelayGrowFactor + + return Math.min(maxReconnectionDelay, Math.round(minDelay + Math.random() * (maxDelay - minDelay))) + }, + + // Schedules a connection attempt to happen after a delay computed according to + // a randomized exponential backoff algorithm variant. + scheduleConnectionAttempt () { + const client = this + + if (!client.shouldReconnect) { + throw new Error('Cannot call `scheduleConnectionAttempt()` when `shouldReconnect` is false.') + } + if (client.nextConnectionAttemptDelayID) { + return console.warn('[pubsub] A reconnection attempt is already scheduled.') + } + const delay = client.getNextRandomDelay() + const nth = client.failedConnectionAttempts + 1 + + client.nextConnectionAttemptDelayID = setTimeout(() => { + sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_ATTEMPT, client) + client.nextConnectionAttemptDelayID = undefined + client.connect() + }, delay) + sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_SCHEDULED, client, { delay, nth }) + }, + + // Unused for now. + pub (contractID: string, data: JSONType, dontBroadcast = false) { + }, + + /** + * Sends a SUB request to the server as soon as possible. + * + * - The given contract ID will be cached until we get a relevant server + * response, allowing us to resend the same request if necessary. + * - Any identical UNSUB request that has not been sent yet will be cancelled. + * - Calling this method again before the server has responded has no effect. + * @param contractID - The ID of the contract whose updates we want to subscribe to. + */ + sub (contractID: string, dontBroadcast = false) { + const client = this + const { socket } = this + + if (!client.pendingSubscriptionSet.has(contractID)) { + client.pendingSubscriptionSet.add(contractID) + client.pendingUnsubscriptionSet.delete(contractID) + + if (socket?.readyState === WebSocket.OPEN) { + socket.send(createRequest(REQUEST_TYPE.SUB, { contractID }, dontBroadcast)) + } + } + }, + + /** + * Sends an UNSUB request to the server as soon as possible. + * + * - The given contract ID will be cached until we get a relevant server + * response, allowing us to resend the same request if necessary. + * - Any identical SUB request that has not been sent yet will be cancelled. + * - Calling this method again before the server has responded has no effect. + * @param contractID - The ID of the contract whose updates we want to unsubscribe from. + */ + unsub (contractID: string, dontBroadcast = false) { + const client = this + const { socket } = this + + if (!client.pendingUnsubscriptionSet.has(contractID)) { + client.pendingSubscriptionSet.delete(contractID) + client.pendingUnsubscriptionSet.add(contractID) + + if (socket?.readyState === WebSocket.OPEN) { + socket.send(createRequest(REQUEST_TYPE.UNSUB, { contractID }, dontBroadcast)) + } + } + } +} + +// Register custom SBP event listeners before the first connection. +for (const name of Object.keys(defaultClientEventHandlers)) { + if (name === 'error' || !socketEventNames.includes(name)) { + sbp('okTurtles.events/on', `pubsub-${name}`, (target, detail?: Object) => { + target.listeners[name]({ type: name, target, detail }) + }) + } +} + +export default { + NOTIFICATION_TYPE, + REQUEST_TYPE, + RESPONSE_TYPE, + createClient, + createMessage, + createRequest +} diff --git a/deno-shared/string.ts b/deno-shared/string.ts new file mode 100644 index 0000000000..27eedf995d --- /dev/null +++ b/deno-shared/string.ts @@ -0,0 +1,35 @@ +export function dasherize (s: string): string { + return s + .trim() + .replace(/[_\s]+/g, '-') + .replace(/([A-Z])/g, '-$1') + .replace(/-+/g, '-') + .toLowerCase() +} + +export function capitalize (s: string): string { + return s.substr(0, 1).toUpperCase() + s.substring(1).toLowerCase() +} + +export function camelize (s: string): string { + return s.trim().replace(/(-|_|\s)+(.)?/g, (mathc, sep, c) => { + return c ? c.toUpperCase() : '' + }) +} + +export function startsWith (s: string, what: string): boolean { + return s.indexOf(what) === 0 +} + +export function endsWith (s: string, what: string): boolean { + const len = s.length - what.length + return len >= 0 && s.indexOf(what, len) === len +} + +export function chompLeft (s: string, what: string): string { + return s.indexOf(what) === 0 ? s.slice(what.length) : s +} + +export function chompRight (s: string, what: string): string { + return endsWith(s, what) ? s.slice(0, s.length - what.length) : s +} diff --git a/import-map.json b/import-map.json new file mode 100644 index 0000000000..90ebd6df8a --- /dev/null +++ b/import-map.json @@ -0,0 +1,17 @@ +{ + "imports": { + "~/backend/": "./deno-backend/", + "~/shared/": "./deno-shared/", + "~/": "./", + "@sbp/": "https://cdn.skypack.dev/@sbp/", + + "fmt/": "https://deno.land/std@0.138.0/fmt/", + "path": "https://deno.land/std@0.138.0/path/mod.ts", + + "blakejs": "https://cdn.skypack.dev/blakejs", + "buffer": "https://cdn.skypack.dev/buffer", + "multihashes": "https://esm.sh/multihashes", + "pogo/": "../pogo/", + "tweetnacl": "https://cdn.skypack.dev/tweetnacl" + } +} diff --git a/shared/domains/chelonia/internals.js b/shared/domains/chelonia/internals.js index 96d68493f8..c4f8d56393 100644 --- a/shared/domains/chelonia/internals.js +++ b/shared/domains/chelonia/internals.js @@ -68,8 +68,11 @@ sbp('sbp/selectors/register', { 'chelonia/private/out/eventsSince': async function (contractID: string, since: string) { const events = await fetch(`${this.config.connectionURL}/events/${contractID}/${since}`) .then(handleFetchResult('json')) + // console.log('fetched events:', events) if (Array.isArray(events)) { return events.reverse().map(b64ToStr) + } else { + throw new Error('eventsSince: `events` is not Array') } }, 'chelonia/private/in/processMessage': function (message: GIMessage, state: Object) { diff --git a/shared/pubsub.js b/shared/pubsub.js index edb21d3948..a0ef03346f 100644 --- a/shared/pubsub.js +++ b/shared/pubsub.js @@ -289,7 +289,7 @@ const defaultClientEventHandlers = { // Reset the connection attempt counter so that we'll start a new // reconnection loop when we are back online. client.failedConnectionAttempts = 0 - client.socket?.close() + client.socket?.close(4002, 'offline') }, online (event: Event) { @@ -320,7 +320,8 @@ const defaultClientEventHandlers = { // It will close the connection if we don't get any message from the server. if (options.pingTimeout > 0 && options.pingTimeout < Infinity) { client.pingTimeoutID = setTimeout(() => { - client.socket?.close() + console.debug('[pubsub] Closing the connection because of ping timeout') + client.socket?.close(4000, 'timeout') }, options.pingTimeout) } // We only need to handle contract resynchronization here when reconnecting. @@ -373,7 +374,7 @@ const defaultMessageHandlers = { // Refresh the ping timer, waiting for the next ping. clearTimeout(client.pingTimeoutID) client.pingTimeoutID = setTimeout(() => { - client.socket?.close() + client.socket?.close(4000, 'timeout') }, client.options.pingTimeout) }, @@ -545,7 +546,7 @@ const publicMethods = { for (const name of socketEventNames) { client.socket.removeEventListener(name, client.listeners[name]) } - client.socket.close() + client.socket.close(4001, 'destroy') } client.listeners = {} client.socket = null diff --git a/test/backend.test.js b/test/backend.test.js index 627eb60bce..2f0dcbd9c2 100644 --- a/test/backend.test.js +++ b/test/backend.test.js @@ -63,6 +63,7 @@ sbp('chelonia/configure', { stateSelector: 'state/vuex/state', skipSideEffects: true, connectionOptions: { + pingTimeout: 0, reconnectOnDisconnection: false, reconnectOnOnline: false, reconnectOnTimeout: false, @@ -328,7 +329,9 @@ describe('Full walkthrough', function () { const hash = blake32Hash(buffer) console.log(`hash for ${path.basename(filepath)}: ${hash}`) form.append('hash', hash) - form.append('data', new Blob([buffer]), path.basename(filepath)) + const blob = new Blob([buffer]) + console.log(blob); + form.append('data', blob, path.basename(filepath)) await fetch(`${process.env.API_URL}/file`, { method: 'POST', body: form }) .then(handleFetchResult('text')) .then(r => should(r).equal(`/file/${hash}`)) From ad2c7e8b19420cc116e21b544016fec0766c2524 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sat, 27 Aug 2022 20:10:41 +0200 Subject: [PATCH 02/43] Use own Pogo fork --- deno-backend/routes.ts | 2 +- deno-backend/server.ts | 11 ++++++++++- import-map.json | 5 +++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/deno-backend/routes.ts b/deno-backend/routes.ts index 68faf73533..0d99e5c163 100644 --- a/deno-backend/routes.ts +++ b/deno-backend/routes.ts @@ -4,7 +4,7 @@ import { blake32Hash } from '~/shared/functions.ts' import { SERVER_INSTANCE } from './instance-keys.ts' import { badRequest } from 'pogo/lib/bang.ts' -import Router from 'pogo/lib/router.ts' +import { Router } from 'pogo' import './database.ts' import * as pathlib from 'path' diff --git a/deno-backend/server.ts b/deno-backend/server.ts index 8fe8e82a15..694d4686d9 100644 --- a/deno-backend/server.ts +++ b/deno-backend/server.ts @@ -1,7 +1,7 @@ import { blue, bold, gray } from "fmt/colors.ts" import * as http from 'https://deno.land/std@0.132.0/http/server.ts'; -import pogo from 'pogo/main.ts'; +import pogo from 'pogo'; import sbp from "@sbp/sbp" import GiAuth from './auth.ts' @@ -59,6 +59,15 @@ const pubsub = createServer({ const pogoServer = pogo.server({ hostname: 'localhost', port: Number.parseInt(API_PORT), + onPreResponse (response) { + try { + if (typeof response.header === 'function') { + response.header('X-Frame-Options', 'deny') + } + } catch (err) { + console.warn('could not set X-Frame-Options header:', err.message) + } + } }) // Patch the Pogo server to add WebSocket support. diff --git a/import-map.json b/import-map.json index 90ebd6df8a..6592d45d78 100644 --- a/import-map.json +++ b/import-map.json @@ -11,7 +11,8 @@ "blakejs": "https://cdn.skypack.dev/blakejs", "buffer": "https://cdn.skypack.dev/buffer", "multihashes": "https://esm.sh/multihashes", - "pogo/": "../pogo/", + "pogo": "https://raw.githubusercontent.com/snowteamer/pogo/master/main.ts", + "pogo/": "https://raw.githubusercontent.com/snowteamer/pogo/master/", "tweetnacl": "https://cdn.skypack.dev/tweetnacl" } -} +} \ No newline at end of file From 3110298068338e152bc14188e51ecf1f3a2230ea Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 28 Aug 2022 19:51:28 +0200 Subject: [PATCH 03/43] Drop Nodejs backend --- Gruntfile.js | 64 +- backend/auth.js | 45 -- {deno-backend => backend}/auth.ts | 2 +- backend/database.js | 198 ------- {deno-backend => backend}/database.ts | 79 ++- backend/events.js | 1 - {deno-backend => backend}/events.ts | 0 backend/index.js | 76 --- {deno-backend => backend}/index.ts | 11 + backend/instance-keys.js | 2 - {deno-backend => backend}/instance-keys.ts | 0 backend/pubsub.js | 336 ----------- {deno-backend => backend}/pubsub.ts | 2 +- {deno-backend => backend}/router.ts | 0 backend/routes.js | 249 -------- {deno-backend => backend}/routes.ts | 73 ++- backend/server.js | 106 ---- {deno-backend => backend}/server.ts | 11 +- {deno-backend => backend}/types.ts | 0 cypress.json | 2 +- deno-shared/declarations.ts | 66 --- deno-shared/domains/chelonia/GIMessage.ts | 120 ---- deno-shared/domains/chelonia/chelonia.ts | 379 ------------ deno-shared/domains/chelonia/db.ts | 87 --- deno-shared/domains/chelonia/errors.ts | 41 -- deno-shared/domains/chelonia/events.ts | 3 - deno-shared/domains/chelonia/internals.ts | 332 ----------- deno-shared/functions.ts | 45 -- deno-shared/giLodash.ts | 214 ------- deno-shared/pubsub.test.ts | 52 -- deno-shared/pubsub.ts | 660 --------------------- deno-shared/string.ts | 35 -- import-map.json | 4 +- shared/domains/chelonia/GIMessage.js | 82 ++- shared/domains/chelonia/chelonia.js | 85 +-- shared/domains/chelonia/db.js | 32 +- shared/domains/chelonia/errors.js | 8 +- shared/domains/chelonia/internals.js | 42 +- shared/functions.js | 35 +- shared/pubsub.js | 94 +-- shared/string.js | 14 +- shared/types.js | 19 - test/backend.test.js | 1 - 43 files changed, 301 insertions(+), 3406 deletions(-) delete mode 100644 backend/auth.js rename {deno-backend => backend}/auth.ts (96%) delete mode 100644 backend/database.js rename {deno-backend => backend}/database.ts (66%) delete mode 100644 backend/events.js rename {deno-backend => backend}/events.ts (100%) delete mode 100644 backend/index.js rename {deno-backend => backend}/index.ts (93%) delete mode 100644 backend/instance-keys.js rename {deno-backend => backend}/instance-keys.ts (100%) delete mode 100644 backend/pubsub.js rename {deno-backend => backend}/pubsub.ts (99%) rename {deno-backend => backend}/router.ts (100%) delete mode 100644 backend/routes.js rename {deno-backend => backend}/routes.ts (73%) delete mode 100644 backend/server.js rename {deno-backend => backend}/server.ts (91%) rename {deno-backend => backend}/types.ts (100%) delete mode 100644 deno-shared/declarations.ts delete mode 100644 deno-shared/domains/chelonia/GIMessage.ts delete mode 100644 deno-shared/domains/chelonia/chelonia.ts delete mode 100644 deno-shared/domains/chelonia/db.ts delete mode 100644 deno-shared/domains/chelonia/errors.ts delete mode 100644 deno-shared/domains/chelonia/events.ts delete mode 100644 deno-shared/domains/chelonia/internals.ts delete mode 100644 deno-shared/functions.ts delete mode 100644 deno-shared/giLodash.ts delete mode 100644 deno-shared/pubsub.test.ts delete mode 100644 deno-shared/pubsub.ts delete mode 100644 deno-shared/string.ts diff --git a/Gruntfile.js b/Gruntfile.js index 410607614b..7b1f890b68 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -5,7 +5,7 @@ // // Ensures: // -// - Babel support is available on the backend, in Mocha tests, etc. +// - Babel support is available in Mocha tests, etc. // - Environment variables are set to different values depending // on whether we're in a production environment or otherwise. // @@ -14,7 +14,7 @@ const util = require('util') const chalk = require('chalk') const crypto = require('crypto') -const { exec, fork } = require('child_process') +const { exec } = require('child_process') const execP = util.promisify(exec) const { copyFile, readFile } = require('fs/promises') const fs = require('fs') @@ -66,7 +66,7 @@ const { EXPOSE_SBP = '' } = process.env -const backendIndex = './backend/index.js' +// const backendIndex = './backend/index.ts' const distAssets = 'dist/assets' const distCSS = 'dist/assets/css' const distDir = 'dist' @@ -356,6 +356,7 @@ module.exports = (grunt) => { }, exec: { + deno: 'deno run --allow-env --allow-net --allow-read --allow-write --import-map=import-map.json --no-check backend/index.ts', eslint: 'node ./node_modules/eslint/bin/eslint.js --cache "**/*.{js,vue}"', flow: '"./node_modules/.bin/flow" --quiet || echo The Flow check failed!', puglint: '"./node_modules/.bin/pug-lint-vue" frontend/views', @@ -376,54 +377,7 @@ module.exports = (grunt) => { // Grunt Tasks // ------------------------------------------------------------------------- - let child = null - - // Useful helper task for `grunt test`. - grunt.registerTask('backend:launch', '[internal]', function () { - const done = this.async() - grunt.log.writeln('backend: launching...') - // Provides Babel support for the backend files. - require('@babel/register') - require(backendIndex).then(done).catch(done) - }) - - // Used with `grunt dev` only, makes it possible to restart just the server when - // backend or shared files are modified. - grunt.registerTask('backend:relaunch', '[internal]', function () { - const done = this.async() // Tell Grunt we're async. - const fork2 = function () { - grunt.log.writeln('backend: forking...') - child = fork(backendIndex, process.argv, { - env: process.env, - execArgv: ['--require', '@babel/register'] - }) - child.on('error', (err) => { - if (err) { - console.error('error starting or sending message to child:', err) - process.exit(1) - } - }) - child.on('exit', (c) => { - if (c !== 0) { - grunt.log.error(`child exited with error code: ${c}`.bold) - // ^C can cause c to be null, which is an OK error. - process.exit(c || 0) - } - }) - done() - } - if (child) { - grunt.log.writeln('Killing child!') - // Wait for successful shutdown to avoid EADDRINUSE errors. - child.on('message', () => { - child = null - fork2() - }) - child.send({ shutdown: 1 }) - } else { - fork2() - } - }) + const child = null grunt.registerTask('build', function () { const esbuild = this.flags.watch ? 'esbuild:watch' : 'esbuild' @@ -483,7 +437,7 @@ module.exports = (grunt) => { grunt.registerTask('default', ['dev']) // TODO: add 'deploy' as per https://github.com/okTurtles/group-income/issues/10 - grunt.registerTask('dev', ['checkDependencies', 'exec:chelDeployAll', 'build:watch', 'backend:relaunch', 'keepalive']) + grunt.registerTask('dev', ['checkDependencies', 'exec:chelDeployAll', 'build:watch', 'exec:deno', 'keepalive']) grunt.registerTask('dist', ['build']) // -------------------- @@ -547,7 +501,7 @@ module.exports = (grunt) => { ;[ [['Gruntfile.js'], [eslint]], - [['backend/**/*.js', 'shared/**/*.js'], [eslint, 'backend:relaunch']], + [['backend/**/*.ts', 'shared/**/*.js'], [eslint, 'exec:deno']], [['frontend/**/*.html'], ['copy']], [['frontend/**/*.js'], [eslint]], [['frontend/assets/{fonts,images}/**/*'], ['copy']], @@ -628,8 +582,8 @@ module.exports = (grunt) => { killKeepAlive = this.async() }) - grunt.registerTask('test', ['build', 'exec:chelDeployAll', 'backend:launch', 'exec:test', 'cypress']) - grunt.registerTask('test:unit', ['backend:launch', 'exec:test']) + grunt.registerTask('test', ['build', 'exec:chelDeployAll', 'exec:deno', 'exec:test', 'cypress']) + grunt.registerTask('test:unit', ['exec:deno', 'exec:test']) // ------------------------------------------------------------------------- // Process event handlers diff --git a/backend/auth.js b/backend/auth.js deleted file mode 100644 index dfd58f22c5..0000000000 --- a/backend/auth.js +++ /dev/null @@ -1,45 +0,0 @@ -// create our auth plugin. see: -// https://hapijs.com/tutorials/auth -// https://hapijs.com/tutorials/plugins - -import { verify, b64ToStr } from '~/shared/functions.js' - -const Boom = require('@hapi/boom') - -exports.plugin = { - name: 'gi-auth', - register: function (server: Object, opts: Object) { - server.auth.scheme('gi-auth', function (server, options) { - return { - authenticate: function (request, h) { - const { authorization } = request.headers - if (!authorization) h.unauthenticated(Boom.unauthorized('Missing authorization')) - - let [scheme, json] = authorization.split(/\s+/) - // NOTE: if you want to add any signature verification, do it here - // eslint-disable-next-line no-constant-condition - if (false) { - if (!scheme.includes('gi')) h.unauthenticated(Boom.badRequest('Bad authentication')) - - try { - json = JSON.parse(b64ToStr(json)) - } catch (e) { - return h.unauthenticated(Boom.badRequest('Invalid token format')) - } - // http://hapijs.com/api/#serverauthschemename-scheme - const isValid = verify(json.msg, json.key, json.sig) - json.userId = json.key - const credentials = { credentials: json } - if (!isValid) return h.unauthenticated(Boom.unauthorized('Bad credentials'), credentials) - return h.authenticated(credentials) - } else { - // remove this if you decide to implement it - return h.authenticated({ credentials: 'TODO: delete me' }) - } - } - } - }) - - server.auth.strategy('gi-auth', 'gi-auth') - } -} diff --git a/deno-backend/auth.ts b/backend/auth.ts similarity index 96% rename from deno-backend/auth.ts rename to backend/auth.ts index a7ec563084..0e4558b917 100644 --- a/deno-backend/auth.ts +++ b/backend/auth.ts @@ -4,7 +4,7 @@ const { AlreadyExists, BadRequest, NotFound, PermissionDenied } = Deno.errors -import { verify, b64ToStr } from '~/shared/functions.ts' +import { verify, b64ToStr } from '~/shared/functions.js' export default { name: 'gi-auth', diff --git a/backend/database.js b/backend/database.js deleted file mode 100644 index 28854516a4..0000000000 --- a/backend/database.js +++ /dev/null @@ -1,198 +0,0 @@ -'use strict' - -import sbp from '@sbp/sbp' -import { strToB64 } from '~/shared/functions.js' -import { Readable } from 'stream' -import fs from 'fs' -import util from 'util' -import path from 'path' -import '@sbp/okturtles.data' -import '~/shared/domains/chelonia/db.js' - -const Boom = require('@hapi/boom') - -const writeFileAsync = util.promisify(fs.writeFile) -const readFileAsync = util.promisify(fs.readFile) -const dataFolder = path.resolve('./data') - -if (!fs.existsSync(dataFolder)) { - fs.mkdirSync(dataFolder, { mode: 0o750 }) -} - -const production = process.env.NODE_ENV === 'production' - -export default (sbp('sbp/selectors/register', { - 'backend/db/streamEntriesSince': async function (contractID: string, hash: string): Promise<*> { - let currentHEAD = await sbp('chelonia/db/latestHash', contractID) - if (!currentHEAD) { - throw Boom.notFound(`contractID ${contractID} doesn't exist!`) - } - let prefix = '[' - // NOTE: if this ever stops working you can also try Readable.from(): - // https://nodejs.org/api/stream.html#stream_stream_readable_from_iterable_options - return new Readable({ - async read (): any { - try { - const entry = await sbp('chelonia/db/getEntry', currentHEAD) - const json = `"${strToB64(entry.serialize())}"` - if (currentHEAD !== hash) { - this.push(prefix + json) - currentHEAD = entry.message().previousHEAD - prefix = ',' - } else { - this.push(prefix + json + ']') - this.push(null) - } - } catch (e) { - console.error(`read(): ${e.message}:`, e) - this.push(']') - this.push(null) - } - } - }) - }, - 'backend/db/streamEntriesBefore': async function (before: string, limit: number): Promise<*> { - let prefix = '[' - let currentHEAD = before - let entry = await sbp('chelonia/db/getEntry', currentHEAD) - if (!entry) { - throw Boom.notFound(`entry ${currentHEAD} doesn't exist!`) - } - limit++ // to return `before` apart from the `limit` number of events - // NOTE: if this ever stops working you can also try Readable.from(): - // https://nodejs.org/api/stream.html#stream_stream_readable_from_iterable_options - return new Readable({ - async read (): any { - try { - if (!currentHEAD || !limit) { - this.push(']') - this.push(null) - } else { - entry = await sbp('chelonia/db/getEntry', currentHEAD) - const json = `"${strToB64(entry.serialize())}"` - this.push(prefix + json) - prefix = ',' - limit-- - currentHEAD = entry.message().previousHEAD - } - } catch (e) { - // TODO: properly return an error to caller, see https://nodejs.org/api/stream.html#errors-while-reading - console.error(`read(): ${e.message}:`, e) - this.push(']') - this.push(null) - } - } - }) - }, - 'backend/db/streamEntriesBetween': async function (startHash: string, endHash: string, offset: number): Promise<*> { - let prefix = '[' - let isMet = false - let currentHEAD = endHash - let entry = await sbp('chelonia/db/getEntry', currentHEAD) - if (!entry) { - throw Boom.notFound(`entry ${currentHEAD} doesn't exist!`) - } - // NOTE: if this ever stops working you can also try Readable.from(): - // https://nodejs.org/api/stream.html#stream_stream_readable_from_iterable_options - return new Readable({ - async read (): any { - try { - entry = await sbp('chelonia/db/getEntry', currentHEAD) - const json = `"${strToB64(entry.serialize())}"` - this.push(prefix + json) - prefix = ',' - - if (currentHEAD === startHash) { - isMet = true - } else if (isMet) { - offset-- - } - - currentHEAD = entry.message().previousHEAD - if (!currentHEAD || (isMet && !offset)) { - this.push(']') - this.push(null) - } - } catch (e) { - // TODO: properly return an error to caller, see https://nodejs.org/api/stream.html#errors-while-reading - console.error(`read(): ${e.message}:`, e) - this.push(']') - this.push(null) - } - } - }) - }, - // ======================= - // wrapper methods to add / lookup names - // ======================= - 'backend/db/registerName': async function (name: string, value: string): Promise<*> { - const exists = await sbp('backend/db/lookupName', name) - if (exists) { - if (!Boom.isBoom(exists)) { - return Boom.conflict('exists') - } else if (exists.output.statusCode !== 404) { - throw exists // throw if this is an error other than "not found" - } - // otherwise it is a Boom.notFound(), proceed ahead - } - await sbp('chelonia/db/set', namespaceKey(name), value) - return { name, value } - }, - 'backend/db/lookupName': async function (name: string): Promise<*> { - const value = await sbp('chelonia/db/get', namespaceKey(name)) - return value || Boom.notFound() - }, - // ======================= - // Filesystem API - // - // TODO: add encryption - // ======================= - 'backend/db/readFile': async function (filename: string): Promise<*> { - const filepath = throwIfFileOutsideDataDir(filename) - if (!fs.existsSync(filepath)) { - return Boom.notFound() - } - return await readFileAsync(filepath) - }, - 'backend/db/writeFile': async function (filename: string, data: any): Promise<*> { - // TODO: check for how much space we have, and have a server setting - // that determines how much of the disk space we're allowed to - // use. If the size of the file would cause us to exceed this - // amount, throw an exception - return await writeFileAsync(throwIfFileOutsideDataDir(filename), data) - }, - 'backend/db/writeFileOnce': async function (filename: string, data: any): Promise<*> { - const filepath = throwIfFileOutsideDataDir(filename) - if (fs.existsSync(filepath)) { - console.warn('writeFileOnce: exists:', filepath) - return - } - return await writeFileAsync(filepath, data) - } -}): any) - -function namespaceKey (name: string): string { - return 'name=' + name -} - -function throwIfFileOutsideDataDir (filename: string): string { - const filepath = path.resolve(path.join(dataFolder, filename)) - if (filepath.indexOf(dataFolder) !== 0) { - throw Boom.badRequest(`bad name: ${filename}`) - } - return filepath -} - -if (production || process.env.GI_PERSIST) { - sbp('sbp/selectors/overwrite', { - // we cannot simply map this to readFile, because 'chelonia/db/getEntry' - // calls this and expects a string, not a Buffer - // 'chelonia/db/get': sbp('sbp/selectors/fn', 'backend/db/readFile'), - 'chelonia/db/get': async function (filename: string) { - const value = await sbp('backend/db/readFile', filename) - return Boom.isBoom(value) ? null : value.toString('utf8') - }, - 'chelonia/db/set': sbp('sbp/selectors/fn', 'backend/db/writeFile') - }) - sbp('sbp/selectors/lock', ['chelonia/db/get', 'chelonia/db/set', 'chelonia/db/delete']) -} diff --git a/deno-backend/database.ts b/backend/database.ts similarity index 66% rename from deno-backend/database.ts rename to backend/database.ts index 1ea7221a6d..a367c28bea 100644 --- a/deno-backend/database.ts +++ b/backend/database.ts @@ -1,9 +1,10 @@ import * as pathlib from 'path' import sbp from "@sbp/sbp" +import { notFound } from 'pogo/lib/bang.ts' -import '~/shared/domains/chelonia/db.ts' -import { strToB64 } from '~/shared/functions.ts' +import '~/shared/domains/chelonia/db.js' +import { strToB64 } from '~/shared/functions.js' const CI = Deno.env.get('CI') const GI_VERSION = Deno.env.get('GI_VERSION') @@ -43,7 +44,7 @@ export default (sbp('sbp/selectors/register', { 'backend/db/streamEntriesSince': async function (contractID: string, hash: string) { let currentHEAD = await sbp('chelonia/db/latestHash', contractID) if (!currentHEAD) { - throw new NotFound(`contractID ${contractID} doesn't exist!`) + throw notFound(`contractID ${contractID} doesn't exist!`) } const chunks = ['['] try { @@ -66,9 +67,75 @@ export default (sbp('sbp/selectors/register', { } catch (error) { console.error(`read(): ${error.message}:`, error) } - const json = chunks.join('') - // console.log('streamEntriesSince text:', json) - return json + return chunks.join('') + }, + 'backend/db/streamEntriesBefore': async function (before: string, limit: number): Promise { + let currentHEAD = before + let entry = await sbp('chelonia/db/getEntry', currentHEAD) + if (!entry) { + throw notFound(`entry ${currentHEAD} doesn't exist!`) + } + limit++ // to return `before` apart from the `limit` number of events + const chunks = ['['] + try { + while (true) { + if (!currentHEAD || !limit) { + chunks[chunks.length] = ']' + break + } + const entry = await sbp('chelonia/db/getEntry', currentHEAD) + if (!entry) { + console.error(`read(): entry ${currentHEAD} no longer exists.`) + chunks[chunks.length] = ']' + break + } + if (chunks.length > 1) chunks[chunks.length] = ',' + chunks[chunks.length] = `"${strToB64(entry.serialize())}"` + + currentHEAD = entry.message().previousHEAD + limit-- + } + } catch (error) { + console.error(`read(): ${error.message}:`, error) + } + return chunks.join('') + }, + 'backend/db/streamEntriesBetween': async function (startHash: string, endHash: string, offset: number): Promise { + let isMet = false + let currentHEAD = endHash + let entry = await sbp('chelonia/db/getEntry', currentHEAD) + if (!entry) { + throw notFound(`entry ${currentHEAD} doesn't exist!`) + } + const chunks = ['['] + try { + while (true) { + const entry = await sbp('chelonia/db/getEntry', currentHEAD) + if (!entry) { + console.error(`read(): entry ${currentHEAD} no longer exists.`) + chunks[chunks.length] = ']' + break + } + if (chunks.length > 1) chunks[chunks.length] = ',' + chunks[chunks.length] = `"${strToB64(entry.serialize())}"` + + if (currentHEAD === startHash) { + isMet = true + } else if (isMet) { + offset-- + } + + currentHEAD = entry.message().previousHEAD + + if (!currentHEAD || (isMet && !offset)) { + chunks[chunks.length] = ']' + break + } + } + } catch (error) { + console.error(`read(): ${error.message}:`, error) + } + return chunks.join('') }, // ======================= // wrapper methods to add / lookup names diff --git a/backend/events.js b/backend/events.js deleted file mode 100644 index e1aa4ed860..0000000000 --- a/backend/events.js +++ /dev/null @@ -1 +0,0 @@ -export const SERVER_RUNNING = 'server-running' diff --git a/deno-backend/events.ts b/backend/events.ts similarity index 100% rename from deno-backend/events.ts rename to backend/events.ts diff --git a/backend/index.js b/backend/index.js deleted file mode 100644 index ce4f2d84d3..0000000000 --- a/backend/index.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict' - -import sbp from '@sbp/sbp' -import '@sbp/okturtles.data' -import '@sbp/okturtles.events' -import { SERVER_RUNNING } from './events.js' -import { PUBSUB_INSTANCE } from './instance-keys.js' -import chalk from 'chalk' - -global.logger = function (err) { - console.error(err) - err.stack && console.error(err.stack) - return err // routes.js is written in a way that depends on this returning the error -} - -const dontLog = { 'backend/server/broadcastEntry': true } - -function logSBP (domain, selector, data) { - if (!dontLog[selector]) { - console.log(chalk.bold(`[sbp] ${selector}`), data) - } -} - -;['backend'].forEach(domain => sbp('sbp/filters/domain/add', domain, logSBP)) -;[].forEach(sel => sbp('sbp/filters/selector/add', sel, logSBP)) - -module.exports = (new Promise((resolve, reject) => { - sbp('okTurtles.events/on', SERVER_RUNNING, function () { - console.log(chalk.bold('backend startup sequence complete.')) - resolve() - }) - // call this after we've registered listener for SERVER_RUNNING - require('./server.js') -}): Promise) - -const shutdownFn = function (message) { - sbp('okTurtles.data/apply', PUBSUB_INSTANCE, function (pubsub) { - console.log('message received in child, shutting down...', message) - pubsub.on('close', async function () { - try { - await sbp('backend/server/stop') - console.log('Hapi server down') - // await db.stop() - // console.log('database stopped') - process.send({}) // tell grunt we've successfully shutdown the server - process.nextTick(() => process.exit(0)) // triple-check we quit :P - } catch (err) { - console.error('Error during shutdown:', err) - process.exit(1) - } - }) - pubsub.close() - // Since `ws` v8.0, `WebSocketServer.close()` no longer closes remaining connections. - // See https://github.com/websockets/ws/commit/df7de574a07115e2321fdb5fc9b2d0fea55d27e8 - pubsub.clients.forEach(client => client.terminate()) - }) -} - -// sent by nodemon -process.on('SIGUSR2', shutdownFn) - -// when spawned via grunt, listen for message to cleanly shutdown and relinquish port -process.on('message', shutdownFn) - -process.on('uncaughtException', (err) => { - console.error('[server] Unhandled exception:', err, err.stack) - process.exit(1) -}) - -process.on('unhandledRejection', (reason, p) => { - console.error('[server] Unhandled promise rejection:', p, 'reason:', reason) - process.exit(1) -}) - -// TODO: should we use Bluebird to handle swallowed errors -// http://jamesknelson.com/are-es6-promises-swallowing-your-errors/ diff --git a/deno-backend/index.ts b/backend/index.ts similarity index 93% rename from deno-backend/index.ts rename to backend/index.ts index c80bf8e883..a15f7c9015 100644 --- a/deno-backend/index.ts +++ b/backend/index.ts @@ -13,6 +13,17 @@ const logger = window.logger = function (err) { return err // routes.js is written in a way that depends on this returning the error } +const process = window.process = { + env: { + get (key) { + return Deno.env.get(key) + }, + set (key, value) { + return Deno.env.set(key, value) + } + } +} + const dontLog = { 'backend/server/broadcastEntry': true } function logSBP (domain, selector, data) { diff --git a/backend/instance-keys.js b/backend/instance-keys.js deleted file mode 100644 index d371506154..0000000000 --- a/backend/instance-keys.js +++ /dev/null @@ -1,2 +0,0 @@ -export const SERVER_INSTANCE = '@instance/server' -export const PUBSUB_INSTANCE = '@instance/pubsub' diff --git a/deno-backend/instance-keys.ts b/backend/instance-keys.ts similarity index 100% rename from deno-backend/instance-keys.ts rename to backend/instance-keys.ts diff --git a/backend/pubsub.js b/backend/pubsub.js deleted file mode 100644 index 78bb004a16..0000000000 --- a/backend/pubsub.js +++ /dev/null @@ -1,336 +0,0 @@ -/* globals logger */ -'use strict' - -/* - * Pub/Sub server implementation using the `ws` library. - * See https://github.com/websockets/ws#api-docs - */ - -import { - NOTIFICATION_TYPE, - REQUEST_TYPE, - RESPONSE_TYPE, - createClient, - createMessage, - messageParser -} from '~/shared/pubsub.js' - -import type { - Message, SubMessage, UnsubMessage, - NotificationTypeEnum, ResponseTypeEnum -} from '~/shared/pubsub.js' - -import type { JSONType } from '~/shared/types.js' - -const { bold } = require('chalk') -const WebSocket = require('ws') - -const { PING, PONG, PUB, SUB, UNSUB } = NOTIFICATION_TYPE -const { ERROR, SUCCESS } = RESPONSE_TYPE - -// Used to tag console output. -const tag = '[pubsub]' - -// ====== Helpers ====== // - -const generateSocketID = (() => { - let counter = 0 - - return (debugID) => String(counter++) + (debugID ? '-' + debugID : '') -})() - -const log = console.log.bind(console, tag) -log.bold = (...args) => console.log(bold(tag, ...args)) -log.debug = console.debug.bind(console, tag) -log.error = (...args) => console.error(bold.red(tag, ...args)) - -// ====== API ====== // - -// Re-export some useful things from the shared module. -export { createClient, createMessage, NOTIFICATION_TYPE, REQUEST_TYPE, RESPONSE_TYPE } - -export function createErrorResponse (data: JSONType): string { - return JSON.stringify({ type: ERROR, data }) -} - -export function createNotification (type: NotificationTypeEnum, data: JSONType): string { - return JSON.stringify({ type, data }) -} - -export function createResponse (type: ResponseTypeEnum, data: JSONType): string { - return JSON.stringify({ type, data }) -} - -/** - * Creates a pubsub server instance. - * - * @param {(http.Server|https.Server)} server - A Node.js HTTP/S server to attach to. - * @param {Object?} options - * {boolean?} logPingRounds - Whether to log ping rounds. - * {boolean?} logPongMessages - Whether to log received pong messages. - * {object?} messageHandlers - Custom handlers for different message types. - * {object?} serverHandlers - Custom handlers for server events. - * {object?} socketHandlers - Custom handlers for socket events. - * {number?} backlog=511 - The maximum length of the queue of pending connections. - * {Function?} handleProtocols - A function which can be used to handle the WebSocket subprotocols. - * {number?} maxPayload=6_291_456 - The maximum allowed message size in bytes. - * {string?} path - Accept only connections matching this path. - * {(boolean|object)?} perMessageDeflate - Enables/disables per-message deflate. - * {number?} pingInterval=30_000 - The time to wait between successive pings. - * @returns {Object} - */ -export function createServer (httpServer: Object, options?: Object = {}): Object { - const server = new WebSocket.Server({ - ...defaultOptions, - ...options, - ...{ clientTracking: true }, - server: httpServer - }) - server.customServerEventHandlers = { ...options.serverHandlers } - server.customSocketEventHandlers = { ...options.socketHandlers } - server.messageHandlers = { ...defaultMessageHandlers, ...options.messageHandlers } - server.pingIntervalID = undefined - server.subscribersByContractID = Object.create(null) - - // Add listeners for server events, i.e. events emitted on the server object. - Object.keys(defaultServerHandlers).forEach((name) => { - server.on(name, (...args) => { - try { - // Always call the default handler first. - defaultServerHandlers[name]?.call(server, ...args) - server.customServerEventHandlers[name]?.call(server, ...args) - } catch (error) { - server.emit('error', error) - } - }) - }) - // Setup a ping interval if required. - if (server.options.pingInterval > 0) { - server.pingIntervalID = setInterval(() => { - if (server.clients.length && server.options.logPingRounds) { - log.debug('Pinging clients') - } - server.clients.forEach((client) => { - if (client.pinged && !client.activeSinceLastPing) { - log(`Disconnecting irresponsive client ${client.id}`) - return client.terminate() - } - if (client.readyState === WebSocket.OPEN) { - client.send(createMessage(PING, Date.now()), () => { - client.activeSinceLastPing = false - client.pinged = true - }) - } - }) - }, server.options.pingInterval) - } - return Object.assign(server, publicMethods) -} - -const defaultOptions = { - logPingRounds: process.env.NODE_ENV === 'development' && !process.env.CI, - logPongMessages: false, - maxPayload: 6 * 1024 * 1024, - pingInterval: 30000 -} - -// Default handlers for server events. -// The `this` binding refers to the server object. -const defaultServerHandlers = { - close () { - log('Server closed') - }, - /** - * Emitted when a connection handshake completes. - * - * @see https://github.com/websockets/ws/blob/master/doc/ws.md#event-connection - * @param {ws.WebSocket} socket - The client socket that connected. - * @param {http.IncomingMessage} request - The underlying Node http GET request. - */ - connection (socket: Object, request: Object) { - const server = this - const url = request.url - const urlSearch = url.includes('?') ? url.slice(url.lastIndexOf('?')) : '' - const debugID = new URLSearchParams(urlSearch).get('debugID') || '' - socket.id = generateSocketID(debugID) - socket.activeSinceLastPing = true - socket.pinged = false - socket.server = server - socket.subscriptions = new Set() - - log.bold(`Socket ${socket.id} connected. Total: ${this.clients.size}`) - - // Add listeners for socket events, i.e. events emitted on a socket object. - ;['close', 'error', 'message', 'ping', 'pong'].forEach((eventName) => { - socket.on(eventName, (...args) => { - // Logging of 'message' events is handled in the default 'message' event handler. - if (eventName !== 'message') { - log(`Event '${eventName}' on socket ${socket.id}`, ...args.map(arg => String(arg))) - } - try { - (defaultSocketEventHandlers: Object)[eventName]?.call(socket, ...args) - socket.server.customSocketEventHandlers[eventName]?.call(socket, ...args) - } catch (error) { - socket.server.emit('error', error) - socket.terminate() - } - }) - }) - }, - error (error: Error) { - log.error('Server error:', error) - logger(error) - }, - headers () { - }, - listening () { - log('Server listening') - } -} - -// Default handlers for server-side client socket events. -// The `this` binding refers to the connected `ws` socket object. -const defaultSocketEventHandlers = { - close (code: string, reason: string) { - const socket = this - const { server, id: socketID } = this - - // Notify other client sockets that this one has left any room they shared. - for (const contractID of socket.subscriptions) { - const subscribers = server.subscribersByContractID[contractID] - // Remove this socket from the subscribers of the given contract. - subscribers.delete(socket) - const notification = createNotification(UNSUB, { contractID, socketID }) - server.broadcast(notification, { to: subscribers }) - } - socket.subscriptions.clear() - }, - - message (data: Buffer | ArrayBuffer | Buffer[], isBinary: boolean) { - const socket = this - const { server } = this - const text = data.toString() - let msg: Message = { type: '' } - - try { - msg = messageParser(text) - } catch (error) { - log.error(`Malformed message: ${error.message}`) - server.rejectMessageAndTerminateSocket(msg, socket) - return - } - // Now that we have successfully parsed the message, we can log it. - if (msg.type !== 'pong' || server.options.logPongMessages) { - log(`Received '${msg.type}' on socket ${socket.id}`, text) - } - // The socket can be marked as active since it just received a message. - socket.activeSinceLastPing = true - const handler = server.messageHandlers[msg.type] - - if (handler) { - try { - handler.call(socket, msg) - } catch (error) { - // Log the error message and stack trace but do not send it to the client. - logger(error) - server.rejectMessageAndTerminateSocket(msg, socket) - } - } else { - log.error(`Unhandled message type: ${msg.type}`) - server.rejectMessageAndTerminateSocket(msg, socket) - } - } -} - -// These handlers receive the connected `ws` socket through the `this` binding. -const defaultMessageHandlers = { - [PONG] (msg: Message) { - const socket = this - // const timestamp = Number(msg.data) - // const latency = Date.now() - timestamp - socket.activeSinceLastPing = true - }, - - [PUB] (msg: Message) { - // Currently unused. - }, - - [SUB] ({ contractID, dontBroadcast }: SubMessage) { - const socket = this - const { server, id: socketID } = this - - if (!socket.subscriptions.has(contractID)) { - log('Already subscribed to', contractID) - // Add the given contract ID to our subscriptions. - socket.subscriptions.add(contractID) - if (!server.subscribersByContractID[contractID]) { - server.subscribersByContractID[contractID] = new Set() - } - const subscribers = server.subscribersByContractID[contractID] - // Add this socket to the subscribers of the given contract. - subscribers.add(socket) - if (!dontBroadcast) { - // Broadcast a notification to every other open subscriber. - const notification = createNotification(SUB, { contractID, socketID }) - server.broadcast(notification, { to: subscribers, except: socket }) - } - } - socket.send(createResponse(SUCCESS, { type: SUB, contractID })) - }, - - [UNSUB] ({ contractID, dontBroadcast }: UnsubMessage) { - const socket = this - const { server, id: socketID } = this - - if (socket.subscriptions.has(contractID)) { - // Remove the given contract ID from our subscriptions. - socket.subscriptions.delete(contractID) - if (server.subscribersByContractID[contractID]) { - const subscribers = server.subscribersByContractID[contractID] - // Remove this socket from the subscribers of the given contract. - subscribers.delete(socket) - if (!dontBroadcast) { - const notification = createNotification(UNSUB, { contractID, socketID }) - // Broadcast a notification to every other open subscriber. - server.broadcast(notification, { to: subscribers, except: socket }) - } - } - } - socket.send(createResponse(SUCCESS, { type: UNSUB, contractID })) - } -} - -const publicMethods = { - /** - * Broadcasts a message, ignoring clients which are not open. - * - * @param message - * @param to - The intended recipients of the message. Defaults to every open client socket. - * @param except - A recipient to exclude. Optional. - */ - broadcast ( - message: Message, - { to, except }: { to?: Iterable, except?: Object } - ) { - const server = this - - for (const client of to || server.clients) { - if (client.readyState === WebSocket.OPEN && client !== except) { - client.send(message) - } - } - }, - - // Enumerates the subscribers of a given contract. - * enumerateSubscribers (contractID: string): Iterable { - const server = this - - if (contractID in server.subscribersByContractID) { - yield * server.subscribersByContractID[contractID] - } - }, - - rejectMessageAndTerminateSocket (request: Message, socket: Object) { - socket.send(createErrorResponse({ ...request }), () => socket.terminate()) - } -} diff --git a/deno-backend/pubsub.ts b/backend/pubsub.ts similarity index 99% rename from deno-backend/pubsub.ts rename to backend/pubsub.ts index 437a708509..377a3e5f5b 100644 --- a/deno-backend/pubsub.ts +++ b/backend/pubsub.ts @@ -4,7 +4,7 @@ import { isWebSocketPingEvent, } from "https://deno.land/std@0.92.0/ws/mod.ts"; -import { messageParser } from '~/shared/pubsub.ts' +import { messageParser } from '~/shared/pubsub.js' const CI = Deno.env.get('CI') const NODE_ENV = Deno.env.get('NODE_ENV') diff --git a/deno-backend/router.ts b/backend/router.ts similarity index 100% rename from deno-backend/router.ts rename to backend/router.ts diff --git a/backend/routes.js b/backend/routes.js deleted file mode 100644 index be6114b222..0000000000 --- a/backend/routes.js +++ /dev/null @@ -1,249 +0,0 @@ -/* globals logger */ - -'use strict' - -import sbp from '@sbp/sbp' -import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' -import { blake32Hash } from '~/shared/functions.js' -import { SERVER_INSTANCE } from './instance-keys.js' -import path from 'path' -import chalk from 'chalk' -import './database.js' - -const Boom = require('@hapi/boom') -const Joi = require('@hapi/joi') - -const route = new Proxy({}, { - get: function (obj, prop) { - return function (path: string, options: Object, handler: Function | Object) { - sbp('okTurtles.data/apply', SERVER_INSTANCE, function (server: Object) { - server.route({ path, method: prop, options, handler }) - }) - } - } -}) - -// RESTful API routes - -// NOTE: We could get rid of this RESTful API and just rely on pubsub.js to do this -// —BUT HTTP2 might be better than websockets and so we keep this around. -// See related TODO in pubsub.js and the reddit discussion link. -route.POST('/event', { - auth: 'gi-auth', - validate: { payload: Joi.string().required() } -}, async function (request, h) { - try { - console.log('/event handler') - const entry = GIMessage.deserialize(request.payload) - await sbp('backend/server/handleEntry', entry) - return entry.hash() - } catch (err) { - if (err.name === 'ChelErrorDBBadPreviousHEAD') { - console.error(chalk.bold.yellow('ChelErrorDBBadPreviousHEAD'), err) - return Boom.conflict(err.message) - } - return logger(err) - } -}) - -route.GET('/eventsSince/{contractID}/{since}', {}, async function (request, h) { - try { - const { contractID, since } = request.params - const stream = await sbp('backend/db/streamEntriesSince', contractID, since) - // "On an HTTP server, make sure to manually close your streams if a request is aborted." - // From: http://knexjs.org/#Interfaces-Streams - // https://github.com/tgriesser/knex/wiki/Manually-Closing-Streams - // Plus: https://hapijs.com/api#request-events - // request.on('disconnect', stream.end.bind(stream)) - // NOTE: since rewriting database.js to remove objection.js and knex, - // we're currently returning a Readable stream, which doesn't have - // '.end'. If there are any issues we can try switching to returning a - // Writable stream. Both types however do have .destroy. - request.events.once('disconnect', stream.destroy.bind(stream)) - return stream - } catch (err) { - return logger(err) - } -}) - -route.GET('/eventsBefore/{before}/{limit}', {}, async function (request, h) { - try { - const { before, limit } = request.params - - if (!before) return Boom.badRequest('missing before') - if (!limit) return Boom.badRequest('missing limit') - if (isNaN(parseInt(limit)) || parseInt(limit) <= 0) return Boom.badRequest('invalid limit') - - const stream = await sbp('backend/db/streamEntriesBefore', before, parseInt(limit)) - request.events.once('disconnect', stream.destroy.bind(stream)) - return stream - } catch (err) { - return logger(err) - } -}) - -route.GET('/eventsBetween/{startHash}/{endHash}', {}, async function (request, h) { - try { - const { startHash, endHash } = request.params - const offset = parseInt(request.query.offset || '0') - - if (!startHash) return Boom.badRequest('missing startHash') - if (!endHash) return Boom.badRequest('missing endHash') - if (isNaN(offset) || offset < 0) return Boom.badRequest('invalid offset') - - const stream = await sbp('backend/db/streamEntriesBetween', startHash, endHash, offset) - request.events.once('disconnect', stream.destroy.bind(stream)) - return stream - } catch (err) { - return logger(err) - } -}) - -route.POST('/name', { - validate: { - payload: Joi.object({ - name: Joi.string().required(), - value: Joi.string().required() - }) - } -}, async function (request, h) { - try { - const { name, value } = request.payload - return await sbp('backend/db/registerName', name, value) - } catch (err) { - return logger(err) - } -}) - -route.GET('/name/{name}', {}, async function (request, h) { - try { - return await sbp('backend/db/lookupName', request.params.name) - } catch (err) { - return logger(err) - } -}) - -route.GET('/latestHash/{contractID}', { - cache: { otherwise: 'no-store' } -}, async function (request, h) { - try { - const { contractID } = request.params - const hash = await sbp('chelonia/db/latestHash', contractID) - if (!hash) { - console.warn(`[backend] latestHash not found for ${contractID}`) - return Boom.notFound() - } - return hash - } catch (err) { - return logger(err) - } -}) - -route.GET('/time', {}, function (request, h) { - return new Date().toISOString() -}) - -// file upload related - -// TODO: if the browser deletes our cache then not everyone -// has a complete copy of the data and can act as a -// new coordinating server... I don't like that. - -const MEGABTYE = 1048576 // TODO: add settings for these -const SECOND = 1000 - -route.POST('/file', { - // TODO: only allow uploads from registered users - payload: { - output: 'data', - multipart: true, - allow: 'multipart/form-data', - failAction: function (request, h, err) { - console.error('failAction error:', err) - return err - }, - maxBytes: 6 * MEGABTYE, // TODO: make this a configurable setting - timeout: 10 * SECOND // TODO: make this a configurable setting - } -}, async function (request, h) { - try { - console.log('FILE UPLOAD!') - console.log(request.payload) - const { hash, data } = request.payload - if (!hash) return Boom.badRequest('missing hash') - if (!data) return Boom.badRequest('missing data') - // console.log('typeof data:', typeof data) - const ourHash = blake32Hash(data) - if (ourHash !== hash) { - console.error(`hash(${hash}) != ourHash(${ourHash})`) - return Boom.badRequest('bad hash!') - } - await sbp('backend/db/writeFileOnce', hash, data) - return '/file/' + hash - } catch (err) { - return logger(err) - } -}) - -route.GET('/file/{hash}', { - cache: { - // Do not set other cache options here, to make sure the 'otherwise' option - // will be used so that the 'immutable' directive gets included. - otherwise: 'public,max-age=31536000,immutable' - }, - files: { - relativeTo: path.resolve('data') - } -}, function (request, h) { - const { hash } = request.params - console.debug(`GET /file/${hash}`) - // Reusing the given `hash` parameter to set the ETag should be faster than - // letting Hapi hash the file to compute an ETag itself. - return h.file(hash, { etagMethod: false }).etag(hash) -}) - -// SPA routes - -route.GET('/assets/{subpath*}', { - ext: { - onPostHandler: { - method (request, h) { - // since our JS is placed under /assets/ and since service workers - // have their scope limited by where they are, we must add this - // header to allow the service worker to function. Details: - // https://w3c.github.io/ServiceWorker/#service-worker-allowed - if (request.path.includes('assets/js/sw-')) { - console.debug('adding header: Service-Worker-Allowed /') - request.response.header('Service-Worker-Allowed', '/') - } - return h.continue - } - } - }, - files: { - relativeTo: path.resolve('dist/assets') - } -}, function (request, h) { - const { subpath } = request.params - const basename = path.basename(subpath) - console.debug(`GET /assets/${subpath}`) - // In the build config we told our bundler to use the `[name]-[hash]-cached` template - // to name immutable assets. This is useful because `dist/assets/` currently includes - // a few files without hash in their name. - if (basename.includes('-cached')) { - return h.file(subpath, { etagMethod: false }) - .etag(basename) - .header('Cache-Control', 'public,max-age=31536000,immutable') - } - // Files like `main.js` or `main.css` should be revalidated before use. Se we use the default headers. - // This should also be suitable for serving unversioned fonts and images. - return h.file(subpath) -}) - -route.GET('/app/{path*}', {}, { - file: path.resolve('./dist/index.html') -}) - -route.GET('/', {}, function (req, h) { - return h.redirect('/app/') -}) diff --git a/deno-backend/routes.ts b/backend/routes.ts similarity index 73% rename from deno-backend/routes.ts rename to backend/routes.ts index 0d99e5c163..6a59cc8f90 100644 --- a/deno-backend/routes.ts +++ b/backend/routes.ts @@ -1,7 +1,10 @@ +/* globals logger, Deno */ + +import { bold, yellow } from 'fmt/colors.ts' + import sbp from '@sbp/sbp' -import { GIMessage } from '~/shared/domains/chelonia/GIMessage.ts' -import { blake32Hash } from '~/shared/functions.ts' -import { SERVER_INSTANCE } from './instance-keys.ts' +import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' +import { blake32Hash } from '~/shared/functions.js' import { badRequest } from 'pogo/lib/bang.ts' import { Router } from 'pogo' @@ -27,8 +30,7 @@ const route = new Proxy({}, { route.POST('/event', async function (request, h) { try { console.log('/event handler') - const payload = await request.raw.text(); - console.log("payload:", payload); + const payload = await request.raw.text() const entry = GIMessage.deserialize(payload) await sbp('backend/server/handleEntry', entry) @@ -43,11 +45,48 @@ route.POST('/event', async function (request, h) { } }) +route.GET('/eventsBefore/{before}/{limit}', async function (request, h) { + try { + const { before, limit } = request.params + console.log('/eventsBefore:', before, limit) + if (!before) return badRequest('missing before') + if (!limit) return badRequest('missing limit') + if (isNaN(parseInt(limit)) || parseInt(limit) <= 0) return badRequest('invalid limit') + + const json = await sbp('backend/db/streamEntriesBefore', before, parseInt(limit)) + // Make sure to close the stream in case of disconnection. + // request.events.once('disconnect', stream.cancel.bind(stream)) + return h.response(json).type('application/json') + } catch (err) { + return logger(err) + } +}) + +route.GET('/eventsBetween/{startHash}/{endHash}', async function (request, h) { + try { + const { startHash, endHash } = request.params + console.log('/eventsBetween:', startHash, endHash) + const offset = parseInt(request.searchParams.get('offset') || '0') + + if (!startHash) return badRequest('missing startHash') + if (!endHash) return badRequest('missing endHash') + if (isNaN(offset) || offset < 0) return badRequest('invalid offset') + + const json = await sbp('backend/db/streamEntriesBetween', startHash, endHash, offset) + // Make sure to close the stream in case of disconnection. + // request.events.once('disconnect', stream.cancel.bind(stream)) + return h.response(json).type('application/json') + } catch (err) { + return logger(err) + } +}) + route.GET('/eventsSince/{contractID}/{since}', async function (request, h) { try { const { contractID, since } = request.params + console.log('/eventsSince:', contractID, since) const json = await sbp('backend/db/streamEntriesSince', contractID, since) - // Make sure to close the stream in case of disconnection." + // Make sure to close the stream in case of disconnection. // request.events.once('disconnect', stream.cancel.bind(stream)) return h.response(json).type('application/json') } catch (err) { @@ -58,7 +97,7 @@ route.GET('/eventsSince/{contractID}/{since}', async function (request, h) { route.POST('/name', async function (request, h) { try { console.debug('/name', request.body) - const payload = await request.raw.json(); + const payload = await request.raw.json() const { name, value } = payload return await sbp('backend/db/registerName', name, value) @@ -85,7 +124,7 @@ route.GET('/latestHash/{contractID}', async function handler (request, h) { request.response.header('cache-control', 'no-store') if (!hash) { console.warn(`[backend] latestHash not found for ${contractID}`) - return new NotFound() + return new Deno.errors.NotFound() } return hash } catch (err) { @@ -104,18 +143,6 @@ route.GET('/time', function (request, h) { // has a complete copy of the data and can act as a // new coordinating server... I don't like that. -const MEGABYTE = 1048576 // TODO: add settings for these -const SECOND = 1000 - -// TODO: only allow uploads from registered users -const fileUploadOptions = { - output: 'data', - multipart: true, - allow: 'multipart/form-data', - maxBytes: 6 * MEGABYTE, // TODO: make this a configurable setting - timeout: 10 * SECOND // TODO: make this a configurable setting -} - route.POST('/file', async function (request, h) { try { console.log('FILE UPLOAD!') @@ -125,7 +152,7 @@ route.POST('/file', async function (request, h) { const hash = formData.get('hash') if (!data) return badRequest('missing data') if (!hash) return badRequest('missing hash') - + const fileData = await new Promise((resolve, reject) => { const fileReader = new FileReader() fileReader.onload = (event) => { @@ -139,10 +166,10 @@ route.POST('/file', async function (request, h) { const ourHash = blake32Hash(new Uint8Array(fileData)) if (ourHash !== hash) { console.error(`hash(${hash}) != ourHash(${ourHash})`) - return new badRequest('bad hash!') + return badRequest('bad hash!') } await sbp('backend/db/writeFileOnce', hash, fileData) - console.log('/file/' + hash); + console.log('/file/' + hash) return '/file/' + hash } catch (err) { console.error(err) diff --git a/backend/server.js b/backend/server.js deleted file mode 100644 index 67c6efd2a0..0000000000 --- a/backend/server.js +++ /dev/null @@ -1,106 +0,0 @@ -'use strict' - -import sbp from '@sbp/sbp' -import './database.js' -import Hapi from '@hapi/hapi' -import GiAuth from './auth.js' -import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' -import { SERVER_RUNNING } from './events.js' -import { SERVER_INSTANCE, PUBSUB_INSTANCE } from './instance-keys.js' -import { createMessage, createNotification, createServer, NOTIFICATION_TYPE } from './pubsub.js' -import chalk from 'chalk' - -const Inert = require('@hapi/inert') - -// NOTE: migration guides for Hapi v16 -> v17: -// https://github.com/hapijs/hapi/issues/3658 -// https://medium.com/yld-engineering-blog/so-youre-thinking-about-updating-your-hapi-js-server-to-v17-b5732ab5bdb8 -// https://futurestud.io/tutorials/hapi-v17-upgrade-guide-your-move-to-async-await - -const hapi = new Hapi.Server({ - // TODO: improve logging and base it on process.env.NODE_ENV - // https://github.com/okTurtles/group-income/issues/32 - // debug: false, // <- Hapi v16 was outputing too many unnecessary debug statements - // // v17 doesn't seem to do this anymore so I've re-enabled the logging - debug: { log: ['error'], request: ['error'] }, - port: process.env.API_PORT, - // See: https://github.com/hapijs/discuss/issues/262#issuecomment-204616831 - routes: { - cors: { - // TODO: figure out if we can live with '*' or if we need to restrict it - origin: ['*'] - // origin: [ - // process.env.API_URL, - // // improve support for browsersync proxy - // ...(process.env.NODE_ENV === 'development' && ['http://localhost:3000']) - // ] - } - } -}) - -// See https://stackoverflow.com/questions/26213255/hapi-set-header-before-sending-response -hapi.ext({ - type: 'onPreResponse', - method: function (request, h) { - try { - // Hapi Boom error responses don't have `.header()`, - // but custom headers can be manually added using `.output.headers`. - // See https://hapi.dev/module/boom/api/. - if (typeof request.response.header === 'function') { - request.response.header('X-Frame-Options', 'deny') - } else { - request.response.output.headers['X-Frame-Options'] = 'deny' - } - } catch (err) { - console.warn(chalk.yellow('[backend] Could not set X-Frame-Options header:', err.message)) - } - return h.continue - } -}) - -sbp('okTurtles.data/set', SERVER_INSTANCE, hapi) - -sbp('sbp/selectors/register', { - 'backend/server/broadcastEntry': async function (entry: GIMessage) { - const pubsub = sbp('okTurtles.data/get', PUBSUB_INSTANCE) - const pubsubMessage = createMessage(NOTIFICATION_TYPE.ENTRY, entry.serialize()) - const subscribers = pubsub.enumerateSubscribers(entry.contractID()) - console.log(chalk.blue.bold(`[pubsub] Broadcasting ${entry.description()}`)) - await pubsub.broadcast(pubsubMessage, { to: subscribers }) - }, - 'backend/server/handleEntry': async function (entry: GIMessage) { - await sbp('chelonia/db/addEntry', entry) - await sbp('backend/server/broadcastEntry', entry) - }, - 'backend/server/stop': function () { - return hapi.stop() - } -}) - -if (process.env.NODE_ENV === 'development' && !process.env.CI) { - hapi.events.on('response', (request, event, tags) => { - console.debug(chalk`{grey ${request.info.remoteAddress}: ${request.method.toUpperCase()} ${request.path} --> ${request.response.statusCode}}`) - }) -} - -sbp('okTurtles.data/set', PUBSUB_INSTANCE, createServer(hapi.listener, { - serverHandlers: { - connection (socket: Object, request: Object) { - if (process.env.NODE_ENV === 'production') { - socket.send(createNotification(NOTIFICATION_TYPE.APP_VERSION, process.env.GI_VERSION)) - } - } - } -})) - -;(async function () { - // https://hapi.dev/tutorials/plugins - await hapi.register([ - { plugin: GiAuth }, - { plugin: Inert } - ]) - require('./routes.js') - await hapi.start() - console.log('Backend server running at:', hapi.info.uri) - sbp('okTurtles.events/emit', SERVER_RUNNING, hapi) -})() diff --git a/deno-backend/server.ts b/backend/server.ts similarity index 91% rename from deno-backend/server.ts rename to backend/server.ts index 694d4686d9..2e692357d3 100644 --- a/deno-backend/server.ts +++ b/backend/server.ts @@ -17,7 +17,7 @@ import { } from '~/backend/pubsub.ts' import { router } from './routes.ts' -import { GIMessage } from '~/shared/domains/chelonia/GIMessage.ts' +import { GIMessage } from '../shared/domains/chelonia/GIMessage.js' const { version } = await import('~/package.json', { assert: { type: "json" }, @@ -78,6 +78,9 @@ const pogoServer = pogo.server({ if (isUpgradeableRequest(request)) { return pubsub.handleUpgradeableRequest(request) } else { + if (NODE_ENV === 'development' && !CI) { + console.debug(grey(`${request.info.remoteAddress}: ${request.toString()} --> ${request.response.status}`)) + } return originalInject(request) } } @@ -86,12 +89,6 @@ pogoServer.router = router console.log('Backend HTTP server listening:', pogoServer.options) -if (NODE_ENV === 'development' && !CI) { - server.events?.on('response', (request, event, tags) => { - console.debug(grey(`${request.info.remoteAddress}: ${request.method.toUpperCase()} ${request.path} --> ${request.response.statusCode}`)) - }) -} - sbp('okTurtles.data/set', PUBSUB_INSTANCE, pubsub) sbp('okTurtles.data/set', SERVER_INSTANCE, pogoServer) diff --git a/deno-backend/types.ts b/backend/types.ts similarity index 100% rename from deno-backend/types.ts rename to backend/types.ts diff --git a/cypress.json b/cypress.json index 72442e3c69..2a5d24440b 100644 --- a/cypress.json +++ b/cypress.json @@ -2,7 +2,7 @@ "baseUrl": "http://localhost:8000", "viewportWidth": 1201, "viewportHeight": 900, - "defaultCommandTimeout": 10000, + "defaultCommandTimeout": 20000, "fixturesFolder": "test/cypress/fixtures", "integrationFolder": "test/cypress/integration", "pluginsFile": "test/cypress/plugins", diff --git a/deno-shared/declarations.ts b/deno-shared/declarations.ts deleted file mode 100644 index 0e0d9dc7a9..0000000000 --- a/deno-shared/declarations.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint no-undef: "off", no-unused-vars: "off" */ -// ======================= -// This file prevents flow from bitching about globals and "Required module not found" -// https://github.com/facebook/flow/issues/2092#issuecomment-232917073 -// -// Note that the modules can (and should) be properly fixed with flow-typed -// https://github.com/okTurtles/group-income/issues/157 -// ======================= - -// TODO: create a script in scripts/ to run flow via grunt-exec -// and have it output (at the end of the run) helpful suggestions -// like how to use `declare module` to ignore .vue requires, -// and also a strong urging to not overdue the types because -// FlowType is a little bit stupid and it can turn into a -// banging-head-on-desk timesink (literally those words). -// Have the script explain which files represent what. - -// Our globals. -declare function logger(err: Error): void -// Nodejs globals. -declare var process: any - -// ======================= -// Fix "Required module not found" in a hackish way. -// TODO: Proper fix is to use: -// https://github.com/okTurtles/group-income/issues/157 -// ======================= -declare module '@hapi/boom' { declare module.exports: any } -declare module '@hapi/hapi' { declare module.exports: any } -declare module '@hapi/inert' { declare module.exports: any } -declare module '@hapi/joi' { declare module.exports: any } -declare module 'blakejs' { declare module.exports: any } -declare module 'buffer' { declare module.exports: any } -declare module 'chalk' { declare module.exports: any } -declare module 'dompurify' { declare module.exports: any } -declare module 'emoji-mart-vue-fast' { declare module.exports: any } -declare module 'emoji-mart-vue-fast/data/apple.json' { declare module.exports: any } -declare module 'form-data' { declare module.exports: any } -declare module 'localforage' { declare module.exports: any } -declare module 'multihashes' { declare module.exports: any } -declare module 'scrypt-async' { declare module.exports: any } -declare module 'tweetnacl' { declare module.exports: any } -declare module 'tweetnacl-util' { declare module.exports: any } -declare module 'vue' { declare module.exports: any } -declare module 'vue-clickaway' { declare module.exports: any } -declare module 'vue-router' { declare module.exports: any } -declare module 'vue-slider-component' { declare module.exports: any } -declare module 'vuelidate' { declare module.exports: any } -declare module 'vuelidate/lib/validators' { declare module.exports: any } -declare module 'vuelidate/lib/validators/maxLength' { declare module.exports: any } -declare module 'vuelidate/lib/validators/required' { declare module.exports: any } -declare module 'vuelidate/lib/validators/sameAs.js' { declare module.exports: any } -declare module 'vuex' { declare module.exports: any } -declare module 'vue2-touch-events' { declare module.exports: any } -declare module 'wicg-inert' { declare module.exports: any } -declare module 'ws' { declare module.exports: any } -declare module '@sbp/sbp' { declare module.exports: any } -declare module '@sbp/okturtles.data' { declare module.exports: any } -declare module '@sbp/okturtles.eventqueue' { declare module.exports: any } -declare module '@sbp/okturtles.events' { declare module.exports: any } - -// Only necessary because `AppStyles.vue` imports it from its script tag rather than its style tag. -declare module '@assets/style/main.scss' { declare module.exports: any } -// Other .js files. -declare module '@utils/blockies.js' { declare module.exports: Object } -declare module '~/frontend/utils/flowTyper.js' { declare module.exports: Object } diff --git a/deno-shared/domains/chelonia/GIMessage.ts b/deno-shared/domains/chelonia/GIMessage.ts deleted file mode 100644 index 109e58886c..0000000000 --- a/deno-shared/domains/chelonia/GIMessage.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { blake32Hash } from '~/shared/functions.ts' - -export class GIMessage { - static OP_CONTRACT = 'c' - static OP_ACTION_ENCRYPTED: 'ae' = 'ae' // e2e-encrypted action - static OP_ACTION_UNENCRYPTED: 'au' = 'au' // publicly readable action - static OP_KEY_ADD = 'ka' // add this key to the list of keys allowed to write to this contract, or update an existing key - static OP_KEY_DEL = 'kd' // remove this key from authorized keys - static OP_PROTOCOL_UPGRADE = 'pu' - static OP_PROP_SET = 'ps' // set a public key/value pair - static OP_PROP_DEL = 'pd' // delete a public key/value pair - - // eslint-disable-next-line camelcase - static createV1_0 ( - contractID = null, - previousHEAD = null, - op, - signatureFn = defaultSignatureFn - ) { - const message = { - version: '1.0.0', - previousHEAD, - contractID, - op, - // the nonce makes it difficult to predict message contents - // and makes it easier to prevent conflicts during development - nonce: Math.random() - } - // NOTE: the JSON strings generated here must be preserved forever. - // do not ever regenerate this message using the contructor. - // instead store it using serialize() and restore it using - // deserialize(). - const messageJSON = JSON.stringify(message) - const value = JSON.stringify({ - message: messageJSON, - sig: signatureFn(messageJSON) - }) - return new this({ - mapping: { key: blake32Hash(value), value }, - message - }) - } - - // TODO: we need signature verification upon decryption somewhere... - static deserialize (value: string): this { - if (!value) throw new Error(`deserialize bad value: ${value}`) - return new this({ - mapping: { key: blake32Hash(value), value }, - message: JSON.parse(JSON.parse(value).message) - }) - } - - constructor ({ mapping, message }: { mapping: Object, message: Object }) { - this._mapping = mapping - this._message = message - // perform basic sanity check - const [type] = this.message().op - switch (type) { - case GIMessage.OP_CONTRACT: - if (!this.isFirstMessage()) throw new Error('OP_CONTRACT: must be first message') - break - case GIMessage.OP_ACTION_ENCRYPTED: - // nothing for now - break - default: - throw new Error(`unsupported op: ${type}`) - } - } - - decryptedValue (fn?: Function): any { - if (!this._decrypted) { - this._decrypted = ( - this.opType() === GIMessage.OP_ACTION_ENCRYPTED && fn !== undefined - ? fn(this.opValue()) - : this.opValue() - ) - } - return this._decrypted - } - - message () { return this._message } - - op () { return this.message().op } - - opType () { return this.op()[0] } - - opValue () { return this.op()[1] } - - description (): string { - const type = this.opType() - let desc = `` - } - - isFirstMessage (): boolean { return !this.message().previousHEAD } - - contractID (): string { return this.message().contractID || this.hash() } - - serialize (): string { return this._mapping.value } - - hash (): string { return this._mapping.key } -} - -function defaultSignatureFn (data: string) { - return { - type: 'default', - sig: blake32Hash(data) - } -} diff --git a/deno-shared/domains/chelonia/chelonia.ts b/deno-shared/domains/chelonia/chelonia.ts deleted file mode 100644 index ce201e56df..0000000000 --- a/deno-shared/domains/chelonia/chelonia.ts +++ /dev/null @@ -1,379 +0,0 @@ -import sbp from '@sbp/sbp' -import '@sbp/okturtles.events' -import '@sbp/okturtles.eventqueue' -import './internals.ts' -import { CONTRACTS_MODIFIED } from './events.ts' -import { createClient, NOTIFICATION_TYPE } from '~/shared/pubsub.ts' -import { merge, cloneDeep, randomHexString, intersection, difference } from '~/shared/giLodash.ts' -// TODO: rename this to ChelMessage -import { GIMessage } from './GIMessage.ts' -import { ChelErrorUnrecoverable } from './errors.ts' - -// TODO: define ChelContractType for /defineContract - -type ChelRegParams = { - contractName: string; - data: Object; - hooks?: { - prepublishContract?: (GIMessage) => void; - prepublish?: (GIMessage) => void; - postpublish?: (GIMessage) => void; - }; - publishOptions?: { maxAttempts: number }; -} - -type ChelActionParams = { - action: string; - contractID: string; - data: Object; - hooks?: { - prepublishContract?: (GIMessage) => void; - prepublish?: (GIMessage) => void; - postpublish?: (GIMessage) => void; - }; - publishOptions?: { maxAttempts: number }; -} - -export { GIMessage } - -export const ACTION_REGEX: RegExp = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ -// ACTION_REGEX.exec('gi.contracts/group/payment/process') -// 0 => 'gi.contracts/group/payment/process' -// 1 => 'gi.contracts/group/payment/' -// 2 => 'gi.contracts/group' -// 3 => 'gi.contracts' -// 4 => 'group' -// 5 => 'payment' - -sbp('sbp/selectors/register', { - // https://www.wordnik.com/words/chelonia - // https://gitlab.okturtles.org/okturtles/group-income/-/wikis/E2E-Protocol/Framework.md#alt-names - 'chelonia/_init': function () { - this.config = { - decryptFn: JSON.parse, // override! - encryptFn: JSON.stringify, // override! - stateSelector: 'chelonia/private/state', // override to integrate with, for example, vuex - whitelisted: (action: string): boolean => !!this.whitelistedActions[action], - reactiveSet: (obj, key, value) => { obj[key] = value; return value }, // example: set to Vue.set - reactiveDel: (obj, key) => { delete obj[key] }, - skipActionProcessing: false, - skipSideEffects: false, - connectionOptions: { - maxRetries: Infinity, // See https://github.com/okTurtles/group-income/issues/1183 - reconnectOnTimeout: true, // can be enabled since we are not doing auth via web sockets - timeout: 5000 - }, - hooks: { - preHandleEvent: null, // async (message: GIMessage) => {} - postHandleEvent: null, // async (message: GIMessage) => {} - processError: null, // (e: Error, message: GIMessage) => {} - sideEffectError: null, // (e: Error, message: GIMessage) => {} - handleEventError: null, // (e: Error, message: GIMessage) => {} - syncContractError: null, // (e: Error, contractID: string) => {} - pubsubError: null // (e:Error, socket: Socket) - } - } - this.state = { - contracts: {}, // contractIDs => { type, HEAD } (contracts we've subscribed to) - pending: [] // prevents processing unexpected data from a malicious server - } - this.contracts = {} - this.whitelistedActions = {} - this.sideEffectStacks = {} // [contractID]: Array<*> - this.sideEffectStack = (contractID: string): Array<*> => { - let stack = this.sideEffectStacks[contractID] - if (!stack) { - this.sideEffectStacks[contractID] = stack = [] - } - return stack - } - }, - 'chelonia/configure': function (config: Object) { - merge(this.config, config) - // merge will strip the hooks off of config.hooks when merging from the root of the object - // because they are functions and cloneDeep doesn't clone functions - merge(this.config.hooks, config.hooks || {}) - }, - 'chelonia/connect': function (): Object { - if (!this.config.connectionURL) throw new Error('config.connectionURL missing') - if (!this.config.connectionOptions) throw new Error('config.connectionOptions missing') - if (this.pubsub) { - this.pubsub.destroy() - } - let pubsubURL = this.config.connectionURL - if (Deno.env.get('NODE_ENV') === 'development') { - // This is temporarily used in development mode to help the server improve - // its console output until we have a better solution. Do not use for auth. - pubsubURL += `?debugID=${randomHexString(6)}` - } - this.pubsub = createClient(pubsubURL, { - ...this.config.connectionOptions, - messageHandlers: { - [NOTIFICATION_TYPE.ENTRY] (msg) { - // We MUST use 'chelonia/private/in/enqueueHandleEvent' to ensure handleEvent() - // is called AFTER any currently-running calls to 'chelonia/contract/sync' - // to prevent gi.db from throwing "bad previousHEAD" errors. - // Calling via SBP also makes it simple to implement 'test/backend.js' - sbp('chelonia/private/in/enqueueHandleEvent', GIMessage.deserialize(msg.data)) - }, - [NOTIFICATION_TYPE.APP_VERSION] (msg) { - const ourVersion = Deno.env.get('GI_VERSION') - const theirVersion = msg.data - - if (ourVersion !== theirVersion) { - sbp('okTurtles.events/emit', NOTIFICATION_TYPE.APP_VERSION, theirVersion) - } - } - } - }) - if (!this.contractsModifiedListener) { - // Keep pubsub in sync (logged into the right "rooms") with 'state.contracts' - this.contractsModifiedListener = () => sbp('chelonia/pubsub/update') - sbp('okTurtles.events/on', CONTRACTS_MODIFIED, this.contractsModifiedListener) - } - return this.pubsub - }, - 'chelonia/defineContract': function (contract: Object) { - if (!ACTION_REGEX.exec(contract.name)) throw new Error(`bad contract name: ${contract.name}`) - if (!contract.metadata) contract.metadata = { validate () {}, create: () => ({}) } - if (!contract.getters) contract.getters = {} - contract.state = (contractID) => sbp(this.config.stateSelector)[contractID] - this.contracts[contract.name] = contract - sbp('sbp/selectors/register', { - // expose getters for Vuex integration and other conveniences - [`${contract.name}/getters`]: () => contract.getters, - // 2 ways to cause sideEffects to happen: by defining a sideEffect function in the - // contract, or by calling /pushSideEffect w/async SBP call. Can also do both. - [`${contract.name}/pushSideEffect`]: (contractID: string, asyncSbpCall: Array<*>) => { - this.sideEffectStack(contractID).push(asyncSbpCall) - } - }) - for (const action in contract.actions) { - contractFromAction(this.contracts, action) // ensure actions are appropriately named - this.whitelistedActions[action] = true - // TODO: automatically generate send actions here using `${action}/send` - // allow the specification of: - // - the optype (e.g. OP_ACTION_(UN)ENCRYPTED) - // - a localized error message - // - whatever keys should be passed in as well - // base it off of the design of encryptedAction() - sbp('sbp/selectors/register', { - [`${action}/process`]: (message: Object, state: Object) => { - const { meta, data, contractID } = message - // TODO: optimize so that you're creating a proxy object only when needed - const gProxy = gettersProxy(state, contract.getters) - state = state || contract.state(contractID) - contract.metadata.validate(meta, { state, ...gProxy, contractID }) - contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) - contract.actions[action].process(message, { state, ...gProxy }) - }, - [`${action}/sideEffect`]: async (message: Object, state: ?Object) => { - const sideEffects = this.sideEffectStack(message.contractID) - while (sideEffects.length > 0) { - const sideEffect = sideEffects.shift() - try { - await sbp(...sideEffect) - } catch (e) { - console.error(`[chelonia] ERROR: '${e.name}' ${e.message}, for pushed sideEffect of ${message.description()}:`, sideEffect) - this.sideEffectStacks[message.contractID] = [] // clear the side effects - throw e - } - } - if (contract.actions[action].sideEffect) { - state = state || contract.state(message.contractID) - const gProxy = gettersProxy(state, contract.getters) - await contract.actions[action].sideEffect(message, { state, ...gProxy }) - } - } - }) - } - }, - // call this manually to resubscribe/unsubscribe from contracts as needed - // if you are using a custom stateSelector and reload the state (e.g. upon login) - 'chelonia/pubsub/update': function () { - const { contracts } = sbp(this.config.stateSelector) - const client = this.pubsub - const subscribedIDs = [...client.subscriptionSet] - const currentIDs = Object.keys(contracts) - const leaveSubscribed = intersection(subscribedIDs, currentIDs) - const toUnsubscribe = difference(subscribedIDs, leaveSubscribed) - const toSubscribe = difference(currentIDs, leaveSubscribed) - // There is currently no need to tell other clients about our sub/unsubscriptions. - const dontBroadcast = true - try { - for (const contractID of toUnsubscribe) { - client.unsub(contractID, dontBroadcast) - } - for (const contractID of toSubscribe) { - client.sub(contractID, dontBroadcast) - } - } catch (e) { - console.error(`[chelonia] pubsub/update: error ${e.name}: ${e.message}`, { toUnsubscribe, toSubscribe }, e) - this.config.hooks.pubsubError?.(e, client) - } - }, - // resolves when all pending actions for these contractID(s) finish - 'chelonia/contract/wait': function (contractIDs?: string | string[]): Promise<*> { - const listOfIds = contractIDs - ? (typeof contractIDs === 'string' ? [contractIDs] : contractIDs) - : Object.keys(sbp(this.config.stateSelector).contracts) - return Promise.all(listOfIds.map(cID => { - return sbp('okTurtles.eventQueue/queueEvent', cID, ['chelonia/private/noop']) - })) - }, - // 'chelonia/contract' - selectors related to injecting remote data and monitoring contracts - // TODO: add an optional parameter to "retain" the contract (see #828) - 'chelonia/contract/sync': function (contractIDs: string | string[]): Promise<*> { - const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs - return Promise.all(listOfIds.map(contractID => { - // enqueue this invocation in a serial queue to ensure - // handleEvent does not get called on contractID while it's syncing, - // but after it's finished. This is used in tandem with - // queuing the 'chelonia/private/in/handleEvent' selector, defined below. - // This prevents handleEvent getting called with the wrong previousHEAD for an event. - return sbp('okTurtles.eventQueue/queueEvent', contractID, [ - 'chelonia/private/in/syncContract', contractID - ]).catch((err) => { - console.error(`[chelonia] failed to sync ${contractID}:`, err) - throw err // re-throw the error - }) - })) - }, - // TODO: implement 'chelonia/contract/release' (see #828) - // safer version of removeImmediately that waits to finish processing events for contractIDs - 'chelonia/contract/remove': function (contractIDs: string | string[]): Promise<*> { - const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs - return Promise.all(listOfIds.map(contractID => { - return sbp('okTurtles.eventQueue/queueEvent', contractID, [ - 'chelonia/contract/removeImmediately', contractID - ]) - })) - }, - // Warning: avoid using this unless you know what you're doing. Prefer using /remove. - 'chelonia/contract/removeImmediately': function (contractID: string) { - const state = sbp(this.config.stateSelector) - this.config.reactiveDel(state.contracts, contractID) - this.config.reactiveDel(state, contractID) - // calling this will make pubsub unsubscribe for events on `contractID` - sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, state.contracts) - }, - 'chelonia/latestContractState': async function (contractID: string) { - const events = await sbp('chelonia/private/out/eventsSince', contractID, contractID) - let state = {} - // fast-path - try { - for (const event of events) { - await sbp('chelonia/private/in/processMessage', GIMessage.deserialize(event), state) - } - return state - } catch (e) { - console.warn(`[chelonia] latestContractState(${contractID}): fast-path failed due to ${e.name}: ${e.message}`) - state = {} - } - // more error-tolerant but slower due to cloning state on each message - for (const event of events) { - const stateCopy = cloneDeep(state) - try { - await sbp('chelonia/private/in/processMessage', GIMessage.deserialize(event), state) - } catch (e) { - console.warn(`[chelonia] latestContractState: '${e.name}': ${e.message} processing:`, event) - if (e instanceof ChelErrorUnrecoverable) throw e - state = stateCopy - } - } - return state - }, - // 'chelonia/out' - selectors that send data out to the server - 'chelonia/out/registerContract': async function (params: ChelRegParams) { - const { contractName, hooks, publishOptions } = params - const contract = this.contracts[contractName] - if (!contract) throw new Error(`contract not defined: ${contractName}`) - const contractMsg = GIMessage.createV1_0(null, null, [ - GIMessage.OP_CONTRACT, - ({ - type: contractName, - keyJSON: 'TODO: add group public key here' - }: GIOpContract) - ]) - hooks && hooks.prepublishContract && hooks.prepublishContract(contractMsg) - await sbp('chelonia/private/out/publishEvent', contractMsg, publishOptions) - const msg = await sbp('chelonia/out/actionEncrypted', { - action: contractName, - contractID: contractMsg.hash(), - data: params.data, - hooks, - publishOptions - }) - return msg - }, - // all of these functions will do both the creation of the GIMessage - // and the sending of it via 'chelonia/private/out/publishEvent' - 'chelonia/out/actionEncrypted': function (params: ChelActionParams): Promise { - return outEncryptedOrUnencryptedAction.call(this, GIMessage.OP_ACTION_ENCRYPTED, params) - }, - 'chelonia/out/actionUnencrypted': function (params: ChelActionParams): Promise { - return outEncryptedOrUnencryptedAction.call(this, GIMessage.OP_ACTION_UNENCRYPTED, params) - }, - 'chelonia/out/keyAdd': async function () { - - }, - 'chelonia/out/keyDel': async function () { - - }, - 'chelonia/out/protocolUpgrade': async function () { - - }, - 'chelonia/out/propSet': async function () { - - }, - 'chelonia/out/propDel': async function () { - - } -}) - -function contractFromAction (contracts: Object, action: string): Object { - const regexResult = ACTION_REGEX.exec(action) - const contract = contracts[(regexResult && regexResult[2]) || null] - if (!contract) throw new Error(`no contract for action named: ${action}`) - return contract -} - -async function outEncryptedOrUnencryptedAction ( - opType: 'ae' | 'au', - params: ChelActionParams -) { - const { action, contractID, data, hooks, publishOptions } = params - const contract = contractFromAction(this.contracts, action) - const state = contract.state(contractID) - const previousHEAD = await sbp('chelonia/private/out/latestHash', contractID) - const meta = contract.metadata.create() - const gProxy = gettersProxy(state, contract.getters) - contract.metadata.validate(meta, { state, ...gProxy, contractID }) - contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) - const unencMessage = ({ action, data, meta }: GIOpActionUnencrypted) - const message = GIMessage.createV1_0(contractID, previousHEAD, [ - opType, - opType === GIMessage.OP_ACTION_UNENCRYPTED ? unencMessage : this.config.encryptFn(unencMessage) - ] - // TODO: add the signature function here to sign the message whether encrypted or not - ) - hooks && hooks.prepublish && hooks.prepublish(message) - await sbp('chelonia/private/out/publishEvent', message, publishOptions) - hooks && hooks.postpublish && hooks.postpublish(message) - return message -} - -// The gettersProxy is what makes Vue-like getters possible. In other words, -// we want to make sure that the getter functions that we defined in each -// contract get passed the 'state' when a getter is accessed. -// The only way to pass in the state is by creating a Proxy object that does -// that for us. This allows us to maintain compatibility with Vue.js and integrate -// the contract getters into the Vue-facing getters. -function gettersProxy (state: Object, getters: Object) { - const proxyGetters = new Proxy({}, { - get (target, prop) { - return getters[prop](state, proxyGetters) - } - }) - return { getters: proxyGetters } -} diff --git a/deno-shared/domains/chelonia/db.ts b/deno-shared/domains/chelonia/db.ts deleted file mode 100644 index ade50a0214..0000000000 --- a/deno-shared/domains/chelonia/db.ts +++ /dev/null @@ -1,87 +0,0 @@ -import sbp from '@sbp/sbp' -import '@sbp/okturtles.data' -import { GIMessage } from './GIMessage.ts' -import { ChelErrorDBBadPreviousHEAD, ChelErrorDBConnection } from './errors.ts' - -const headSuffix = '-HEAD' - -// NOTE: To enable persistence of log use 'sbp/selectors/overwrite' -// to overwrite the following selectors: -sbp('sbp/selectors/unsafe', ['chelonia/db/get', 'chelonia/db/set', 'chelonia/db/delete']) -// NOTE: MAKE SURE TO CALL 'sbp/selectors/lock' after overwriting them! - -const dbPrimitiveSelectors = Deno.env.get('LIGHTWEIGHT_CLIENT') === 'true' - ? { - 'chelonia/db/get': function (key) { - const id = sbp('chelonia/db/contractIdFromLogHEAD', key) - return Promise.resolve(id ? sbp(this.config.stateSelector).contracts[id]?.HEAD : null) - }, - 'chelonia/db/set': function (key, value) { return Promise.resolve(value) }, - 'chelonia/db/delete': function () { return Promise.resolve() } - } - : { - 'chelonia/db/get': function (key: string) { - return Promise.resolve(sbp('okTurtles.data/get', key)) - }, - 'chelonia/db/set': function (key: string, value: string) { - return Promise.resolve(sbp('okTurtles.data/set', key, value)) - }, - 'chelonia/db/delete': function (key: string) { - return Promise.resolve(sbp('okTurtles.data/delete', key)) - } - } - -export default (sbp('sbp/selectors/register', { - ...dbPrimitiveSelectors, - 'chelonia/db/logHEAD': function (contractID: string): string { - return `${contractID}${headSuffix}` - }, - 'chelonia/db/contractIdFromLogHEAD': function (key: string) { - return key.endsWith(headSuffix) ? key.slice(0, -headSuffix.length) : null - }, - 'chelonia/db/latestHash': function (contractID: string) { - return sbp('chelonia/db/get', sbp('chelonia/db/logHEAD', contractID)) - }, - 'chelonia/db/getEntry': async function (hash: string) { - try { - const value: string = await sbp('chelonia/db/get', hash) - if (!value) throw new Error(`no entry for ${hash}!`) - return GIMessage.deserialize(value) - } catch (e) { - throw new ChelErrorDBConnection(`${e.name} during getEntry: ${e.message}`) - } - }, - 'chelonia/db/addEntry': async function (entry: GIMessage) { - try { - const { previousHEAD } = entry.message() - const contractID: string = entry.contractID() - if (await sbp('chelonia/db/get', entry.hash())) { - console.warn(`[chelonia.db] entry exists: ${entry.hash()}`) - return entry.hash() - } - const HEAD = await sbp('chelonia/db/latestHash', contractID) - if (!entry.isFirstMessage() && previousHEAD !== HEAD) { - console.error(`[chelonia.db] bad previousHEAD: ${previousHEAD}! Expected: ${HEAD} for contractID: ${contractID}`) - throw new ChelErrorDBBadPreviousHEAD(`bad previousHEAD: ${previousHEAD}`) - } - await sbp('chelonia/db/set', entry.hash(), entry.serialize()) - await sbp('chelonia/db/set', sbp('chelonia/db/logHEAD', contractID), entry.hash()) - console.debug(`[chelonia.db] HEAD for ${contractID} updated to:`, entry.hash()) - return entry.hash() - } catch (e) { - if (e.name.includes('ErrorDB')) { - throw e // throw the specific type of ErrorDB instance - } - throw new ChelErrorDBConnection(`${e.name} during addEntry: ${e.message}`) - } - }, - 'chelonia/db/lastEntry': async function (contractID: string) { - try { - const hash = await sbp('chelonia/db/latestHash', contractID) - if (!hash) throw new Error(`contract ${contractID} has no latest hash!`) - return sbp('chelonia/db/getEntry', hash) - } catch (e) { - throw new ChelErrorDBConnection(`${e.name} during lastEntry: ${e.message}`) - } - } -})) diff --git a/deno-shared/domains/chelonia/errors.ts b/deno-shared/domains/chelonia/errors.ts deleted file mode 100644 index e4a0d25652..0000000000 --- a/deno-shared/domains/chelonia/errors.ts +++ /dev/null @@ -1,41 +0,0 @@ -export class ChelErrorDBBadPreviousHEAD extends Error { - // ugly boilerplate because JavaScript is stupid - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types - constructor (...params: any[]) { - super(...params) - // this.name = this.constructor.name - this.name = 'ChelErrorDBBadPreviousHEAD' // string literal so minifier doesn't overwrite - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor) - } - } -} -export class ChelErrorDBConnection extends Error { - constructor (...params: any[]) { - super(...params) - this.name = 'ChelErrorDBConnection' - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor) - } - } -} - -export class ChelErrorUnexpected extends Error { - constructor (...params: any[]) { - super(...params) - this.name = 'ChelErrorUnexpected' - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor) - } - } -} - -export class ChelErrorUnrecoverable extends Error { - constructor (...params: any[]) { - super(...params) - this.name = 'ChelErrorUnrecoverable' - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor) - } - } -} diff --git a/deno-shared/domains/chelonia/events.ts b/deno-shared/domains/chelonia/events.ts deleted file mode 100644 index c12268d7c4..0000000000 --- a/deno-shared/domains/chelonia/events.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const CONTRACT_IS_SYNCING = 'contract-is-syncing' -export const CONTRACTS_MODIFIED = 'contracts-modified' -export const EVENT_HANDLED = 'event-handled' diff --git a/deno-shared/domains/chelonia/internals.ts b/deno-shared/domains/chelonia/internals.ts deleted file mode 100644 index 360c6619e0..0000000000 --- a/deno-shared/domains/chelonia/internals.ts +++ /dev/null @@ -1,332 +0,0 @@ -import sbp from '@sbp/sbp' -import './db.ts' -import { GIMessage } from './GIMessage.ts' -import { b64ToStr } from '~/shared/functions.ts' -import { ChelErrorUnexpected, ChelErrorUnrecoverable } from './errors.ts' -import { CONTRACT_IS_SYNCING, CONTRACTS_MODIFIED, EVENT_HANDLED } from './events.ts' - -import { randomIntFromRange, delay, cloneDeep, debounce, pick } from '~/shared/giLodash.ts' - -function handleFetchResult (type: string) { - return function (r: Object) { - if (!r.ok) throw new Error(`${r.status}: ${r.statusText}`) - return r[type]() - } -} - -sbp('sbp/selectors/register', { - // DO NOT CALL ANY OF THESE YOURSELF! - 'chelonia/private/state': function () { - return this.state - }, - // used by, e.g. 'chelonia/contract/wait' - 'chelonia/private/noop': function () {}, - 'chelonia/private/out/publishEvent': async function (entry: GIMessage, { maxAttempts = 3 } = {}) { - const contractID = entry.contractID() - let attempt = 1 - // auto resend after short random delay - // https://github.com/okTurtles/group-income/issues/608 - while (true) { - const r = await fetch(`${this.config.connectionURL}/event`, { - method: 'POST', - body: entry.serialize(), - headers: { - 'Content-Type': 'text/plain', - 'Authorization': 'gi TODO - signature - if needed here - goes here' - } - }) - if (r.ok) { - return r.text() - } - if (r.status === 409) { - if (attempt + 1 > maxAttempts) { - console.error(`[chelonia] failed to publish ${entry.description()} after ${attempt} attempts`, entry) - throw new Error(`publishEvent: ${r.status} - ${r.statusText}. attempt ${attempt}`) - } - // create new entry - const randDelay = randomIntFromRange(0, 1500) - console.warn(`[chelonia] publish attempt ${attempt} of ${maxAttempts} failed. Waiting ${randDelay} msec before resending ${entry.description()}`) - attempt += 1 - await delay(randDelay) // wait half a second before sending it again - // if this isn't OP_CONTRACT, get latestHash, recreate and resend message - if (!entry.isFirstMessage()) { - const previousHEAD = await sbp('chelonia/private/out/latestHash', contractID) - entry = GIMessage.createV1_0(contractID, previousHEAD, entry.op()) - } - } else { - const message = (await r.json())?.message - console.error(`[chelonia] ERROR: failed to publish ${entry.description()}: ${r.status} - ${r.statusText}: ${message}`, entry) - throw new Error(`publishEvent: ${r.status} - ${r.statusText}: ${message}`) - } - } - }, - 'chelonia/private/out/latestHash': function (contractID: string) { - return fetch(`${this.config.connectionURL}/latestHash/${contractID}`, { - cache: 'no-store' - }).then(handleFetchResult('text')) - }, - // TODO: r.body is a stream.Transform, should we use a callback to process - // the events one-by-one instead of converting to giant json object? - // however, note if we do that they would be processed in reverse... - 'chelonia/private/out/eventsSince': async function (contractID: string, since: string) { - const events = await fetch(`${this.config.connectionURL}/events/${contractID}/${since}`) - .then(handleFetchResult('json')) - // console.log('eventsSince:', events) - if (Array.isArray(events)) { - return events.reverse().map(b64ToStr) - } - }, - 'chelonia/private/in/processMessage': function (message: GIMessage, state: Object) { - const [opT, opV] = message.op() - const hash = message.hash() - const contractID = message.contractID() - const config = this.config - if (!state._vm) state._vm = {} - const opFns: { [GIOpType]: (any) => void } = { - [GIMessage.OP_CONTRACT] (v: GIOpContract) { - // TODO: shouldn't each contract have its own set of authorized keys? - if (!state._vm.authorizedKeys) state._vm.authorizedKeys = [] - // TODO: we probably want to be pushing the de-JSON-ified key here - state._vm.authorizedKeys.push({ key: v.keyJSON, context: 'owner' }) - }, - [GIMessage.OP_ACTION_ENCRYPTED] (v: GIOpActionEncrypted) { - if (!config.skipActionProcessing) { - const decrypted = message.decryptedValue(config.decryptFn) - opFns[GIMessage.OP_ACTION_UNENCRYPTED](decrypted) - } - }, - [GIMessage.OP_ACTION_UNENCRYPTED] (v: GIOpActionUnencrypted) { - if (!config.skipActionProcessing) { - const { data, meta, action } = v - if (!config.whitelisted(action)) { - throw new Error(`chelonia: action not whitelisted: '${action}'`) - } - sbp(`${action}/process`, { data, meta, hash, contractID }, state) - } - }, - [GIMessage.OP_PROP_DEL]: notImplemented, - [GIMessage.OP_PROP_SET] (v: GIOpPropSet) { - if (!state._vm.props) state._vm.props = {} - state._vm.props[v.key] = v.value - }, - [GIMessage.OP_KEY_ADD] (v: GIOpKeyAdd) { - // TODO: implement this. consider creating a function so that - // we're not duplicating code in [GIMessage.OP_CONTRACT] - // if (!state._vm.authorizedKeys) state._vm.authorizedKeys = [] - // state._vm.authorizedKeys.push(v) - }, - [GIMessage.OP_KEY_DEL]: notImplemented, - [GIMessage.OP_PROTOCOL_UPGRADE]: notImplemented - } - let processOp = true - if (config.preOp) { - processOp = config.preOp(message, state) !== false && processOp - } - if (config[`preOp_${opT}`]) { - processOp = config[`preOp_${opT}`](message, state) !== false && processOp - } - if (processOp && !config.skipProcessing) { - opFns[opT](opV) - config.postOp && config.postOp(message, state) - config[`postOp_${opT}`] && config[`postOp_${opT}`](message, state) - } - }, - 'chelonia/private/in/enqueueHandleEvent': function (event: GIMessage) { - // make sure handleEvent is called AFTER any currently-running invocations - // to 'chelonia/contract/sync', to prevent gi.db from throwing - // "bad previousHEAD" errors - return sbp('okTurtles.eventQueue/queueEvent', event.contractID(), [ - 'chelonia/private/in/handleEvent', event - ]) - }, - 'chelonia/private/in/syncContract': async function (contractID: string) { - const state = sbp(this.config.stateSelector) - const latest = await sbp('chelonia/private/out/latestHash', contractID) - console.debug(`syncContract: ${contractID} latestHash is: ${latest}`) - // there is a chance two users are logged in to the same machine and must check their contracts before syncing - let recent - if (state.contracts[contractID]) { - recent = state.contracts[contractID].HEAD - } else { - // we're syncing a contract for the first time, make sure to add to pending - // so that handleEvents knows to expect events from this contract - if (!state.contracts[contractID] && !state.pending.includes(contractID)) { - state.pending.push(contractID) - } - } - sbp('okTurtles.events/emit', CONTRACT_IS_SYNCING, contractID, true) - try { - if (latest !== recent) { - console.debug(`[chelonia] Synchronizing Contract ${contractID}: our recent was ${recent || 'undefined'} but the latest is ${latest}`) - // TODO: fetch events from localStorage instead of server if we have them - const events = await sbp('chelonia/private/out/eventsSince', contractID, recent || contractID) - // remove the first element in cases where we are not getting the contract for the first time - state.contracts[contractID] && events.shift() - for (let i = 0; i < events.length; i++) { - // this must be called directly, instead of via enqueueHandleEvent - await sbp('chelonia/private/in/handleEvent', GIMessage.deserialize(events[i])) - } - } else { - console.debug(`[chelonia] contract ${contractID} was already synchronized`) - } - sbp('okTurtles.events/emit', CONTRACT_IS_SYNCING, contractID, false) - } catch (e) { - console.error(`[chelonia] syncContract error: ${e.message}`, e) - sbp('okTurtles.events/emit', CONTRACT_IS_SYNCING, contractID, false) - this.config.hooks.syncContractError?.(e, contractID) - throw e - } - }, - 'chelonia/private/in/handleEvent': async function (message: GIMessage) { - const state = sbp(this.config.stateSelector) - const contractID = message.contractID() - const hash = message.hash() - const { preHandleEvent, postHandleEvent, handleEventError } = this.config.hooks - let processingErrored = false - // Errors in mutations result in ignored messages - // Errors in side effects result in dropped messages to be reprocessed - try { - preHandleEvent && await preHandleEvent(message) - // verify we're expecting to hear from this contract - if (!state.pending.includes(contractID) && !state.contracts[contractID]) { - console.warn(`[chelonia] WARN: ignoring unexpected event ${message.description()}:`, message.serialize()) - throw new ChelErrorUnexpected() - } - // the order the following actions are done is critically important! - // first we make sure we save this message to the db - // if an exception is thrown here we do not need to revert the state - // because nothing has been processed yet - const proceed = await handleEvent.addMessageToDB(message) - if (proceed === false) return - - const contractStateCopy = cloneDeep(state[contractID] || null) - const stateCopy = cloneDeep(pick(state, ['pending', 'contracts'])) - // process the mutation on the state - // IMPORTANT: even though we 'await' processMutation, everything in your - // contract's 'process' function must be synchronous! The only - // reason we 'await' here is to dynamically load any new contract - // source / definitions specified by the GIMessage - try { - await handleEvent.processMutation.call(this, message, state) - } catch (e) { - console.error(`[chelonia] ERROR '${e.name}' in processMutation for ${message.description()}: ${e.message}`, e, message.serialize()) - // we revert any changes to the contract state that occurred, ignoring this mutation - handleEvent.revertProcess.call(this, { message, state, contractID, contractStateCopy }) - processingErrored = true - this.config.hooks.processError?.(e, message) - // special error that prevents the head from being updated, effectively killing the contract - if (e.name === 'ChelErrorUnrecoverable') throw e - } - // whether or not there was an exception, we proceed ahead with updating the head - // you can prevent this by throwing an exception in the processError hook - state.contracts[contractID].HEAD = hash - // process any side-effects (these must never result in any mutation to the contract state!) - if (!processingErrored) { - try { - if (!this.config.skipActionProcessing && !this.config.skipSideEffects) { - await handleEvent.processSideEffects.call(this, message) - } - postHandleEvent && await postHandleEvent(message) - sbp('okTurtles.events/emit', hash, contractID, message) - sbp('okTurtles.events/emit', EVENT_HANDLED, contractID, message) - } catch (e) { - console.error(`[chelonia] ERROR '${e.name}' in side-effects for ${message.description()}: ${e.message}`, e, message.serialize()) - // revert everything - handleEvent.revertSideEffect.call(this, { message, state, contractID, contractStateCopy, stateCopy }) - this.config.hooks.sideEffectError?.(e, message) - throw e // rethrow to prevent the contract sync from going forward - } - } - } catch (e) { - console.error(`[chelonia] ERROR in handleEvent: ${e.message}`, e) - handleEventError?.(e, message) - throw e - } - } -}) - -const eventsToReinjest = [] -const reprocessDebounced = debounce((contractID) => sbp('chelonia/contract/sync', contractID), 1000) - -const handleEvent = { - async addMessageToDB (message: GIMessage) { - const contractID = message.contractID() - const hash = message.hash() - try { - await sbp('chelonia/db/addEntry', message) - const reprocessIdx = eventsToReinjest.indexOf(hash) - if (reprocessIdx !== -1) { - console.warn(`[chelonia] WARN: successfully reinjested ${message.description()}`) - eventsToReinjest.splice(reprocessIdx, 1) - } - } catch (e) { - if (e.name === 'ChelErrorDBBadPreviousHEAD') { - // sometimes we simply miss messages, it's not clear why, but it happens - // in rare cases. So we attempt to re-sync this contract once - if (eventsToReinjest.length > 100) { - throw new ChelErrorUnrecoverable('more than 100 different bad previousHEAD errors') - } - if (!eventsToReinjest.includes(hash)) { - console.warn(`[chelonia] WARN bad previousHEAD for ${message.description()}, will attempt to re-sync contract to reinjest message`) - eventsToReinjest.push(hash) - reprocessDebounced(contractID) - return false // ignore the error for now - } else { - console.error(`[chelonia] ERROR already attempted to reinjest ${message.description()}, will not attempt again!`) - } - } - throw e - } - }, - async processMutation (message: GIMessage, state: Object) { - const contractID = message.contractID() - if (message.isFirstMessage()) { - // Flow doesn't understand that a first message must be a contract, - // so we have to help it a bit in order to acces the 'type' property. - const { type } = ((message.opValue(): any): GIOpContract) - if (!state[contractID]) { - console.debug(`contract ${type} registered for ${contractID}`) - this.config.reactiveSet(state, contractID, {}) - this.config.reactiveSet(state.contracts, contractID, { type, HEAD: contractID }) - } - // we've successfully received it back, so remove it from expectation pending - const index = state.pending.indexOf(contractID) - index !== -1 && state.pending.splice(index, 1) - sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, state.contracts) - } - await Promise.resolve() // TODO: load any unloaded contract code - sbp('chelonia/private/in/processMessage', message, state[contractID]) - }, - async processSideEffects (message: GIMessage) { - if ([GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_ACTION_UNENCRYPTED].includes(message.opType())) { - const contractID = message.contractID() - const hash = message.hash() - const { action, data, meta } = message.decryptedValue() - const mutation = { data, meta, hash, contractID } - await sbp(`${action}/sideEffect`, mutation) - } - }, - revertProcess ({ message, state, contractID, contractStateCopy }) { - console.warn(`[chelonia] reverting mutation ${message.description()}: ${message.serialize()}. Any side effects will be skipped!`) - if (!contractStateCopy) { - console.warn(`[chelonia] mutation reversion on very first message for contract ${contractID}! Your contract may be too damaged to be useful and should be redeployed with bugfixes.`) - contractStateCopy = {} - } - this.config.reactiveSet(state, contractID, contractStateCopy) - }, - revertSideEffect ({ message, state, contractID, contractStateCopy, stateCopy }) { - console.warn(`[chelonia] reverting entire state because failed sideEffect for ${message.description()}: ${message.serialize()}`) - if (!contractStateCopy) { - this.config.reactiveDel(state, contractID) - } else { - this.config.reactiveSet(state, contractID, contractStateCopy) - } - state.contracts = stateCopy.contracts - state.pending = stateCopy.pending - sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, state.contracts) - } -} - -const notImplemented = (v) => { - throw new Error(`chelonia: action not implemented to handle: ${JSON.stringify(v)}.`) -} diff --git a/deno-shared/functions.ts b/deno-shared/functions.ts deleted file mode 100644 index e5ee12a631..0000000000 --- a/deno-shared/functions.ts +++ /dev/null @@ -1,45 +0,0 @@ -import multihash from 'multihashes' -import nacl from 'tweetnacl' -import blake from 'blakejs' - -import { Buffer } from 'buffer' - -export function blake32Hash (data: string | Buffer | Uint8Array): string { - // TODO: for node/electron, switch to: https://github.com/ludios/node-blake2 - const uint8array = blake.blake2b(data, null, 32) - // TODO: if we switch to webpack we may need: https://github.com/feross/buffer - // https://github.com/feross/typedarray-to-buffer - const buf = Buffer.from(uint8array.buffer) - return multihash.toB58String(multihash.encode(buf, 'blake2b-32', 32)) -} - -// NOTE: to preserve consistency across browser and node, we use the Buffer -// class. We could use btoa and atob in web browsers (functions that -// are unavailable on Node.js), but they do not support Unicode, -// and you have to jump through some hoops to get it to work: -// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa#Unicode_strings -// These hoops might result in inconsistencies between Node.js and the frontend. -export const b64ToBuf = (b64: string): Buffer => Buffer.from(b64, 'base64') -export const b64ToStr = (b64: string): string => b64ToBuf(b64).toString('utf8') -export const bufToB64 = (buf: Buffer): string => Buffer.from(buf).toString('base64') -export const strToBuf = (str: string): Buffer => Buffer.from(str, 'utf8') -export const strToB64 = (str: string): string => strToBuf(str).toString('base64') -export const bytesToB64 = (ary: Uint8Array): string => Buffer.from(ary).toString('base64') - -export function sign ( - { publicKey, secretKey }: {publicKey: string, secretKey: string}, - msg: string = 'hello!', - futz: string = '' -): string { - return strToB64(JSON.stringify({ - msg: msg + futz, - key: publicKey, - sig: bytesToB64(nacl.sign.detached(strToBuf(msg), b64ToBuf(secretKey))) - })) -} - -export function verify ( - msg: string, key: string, sig: string -): any { - return nacl.sign.detached.verify(strToBuf(msg), b64ToBuf(sig), b64ToBuf(key)) -} diff --git a/deno-shared/giLodash.ts b/deno-shared/giLodash.ts deleted file mode 100644 index aa255a0374..0000000000 --- a/deno-shared/giLodash.ts +++ /dev/null @@ -1,214 +0,0 @@ -// manually implemented lodash functions are better than even: -// https://github.com/lodash/babel-plugin-lodash -// additional tiny versions of lodash functions are available in VueScript2 - -export function mapValues (obj: Object, fn: Function, o: Object = {}): any { - for (const key in obj) { o[key] = fn(obj[key]) } - return o -} - -export function mapObject (obj: Object, fn: Function): {[any]: any} { - return Object.fromEntries(Object.entries(obj).map(fn)) -} - -export function pick (o: Object, props: string[]): Object { - const x = {} - for (const k of props) { x[k] = o[k] } - return x -} - -export function pickWhere (o: Object, where: Function): Object { - const x = {} - for (const k in o) { - if (where(o[k])) { x[k] = o[k] } - } - return x -} - -export function choose (array: Array<*>, indices: Array): Array<*> { - const x = [] - for (const idx of indices) { x.push(array[idx]) } - return x -} - -export function omit (o: Object, props: string[]): {...} { - const x = {} - for (const k in o) { - if (!props.includes(k)) { - x[k] = o[k] - } - } - return x -} - -export function cloneDeep (obj: Object): any { - return JSON.parse(JSON.stringify(obj)) -} - -function isMergeableObject (val) { - const nonNullObject = val && typeof val === 'object' - // $FlowFixMe - return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]' -} - -export function merge (obj: Object, src: Object): any { - for (const key in src) { - const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined - if (clone && isMergeableObject(obj[key])) { - merge(obj[key], clone) - continue - } - obj[key] = clone || src[key] - } - return obj -} - -export function delay (msec: number): Promise { - return new Promise((resolve, reject) => { - setTimeout(resolve, msec) - }) -} - -export function randomBytes (length: number): Uint8Array { - // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019 - return crypto.getRandomValues(new Uint8Array(length)) -} - -export function randomHexString (length: number): string { - return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('') -} - -export function randomIntFromRange (min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1) + min) -} - -export function randomFromArray (arr: any[]): any { - return arr[Math.floor(Math.random() * arr.length)] -} - -export function flatten (arr: Array<*>): Array { - let flat: Array<*> = [] - for (let i = 0; i < arr.length; i++) { - if (Array.isArray(arr[i])) { - flat = flat.concat(arr[i]) - } else { - flat.push(arr[i]) - } - } - return flat -} - -export function zip (): any[] { - // $FlowFixMe - const arr = Array.prototype.slice.call(arguments) - const zipped = [] - let max = 0 - arr.forEach((current) => (max = Math.max(max, current.length))) - for (const current of arr) { - for (let i = 0; i < max; i++) { - zipped[i] = zipped[i] || [] - zipped[i].push(current[i]) - } - } - return zipped -} - -export function uniq (array: any[]): any[] { - return Array.from(new Set(array)) -} - -export function union (...arrays: any[][]): any[] { - // $FlowFixMe - return uniq([].concat.apply([], arrays)) -} - -export function intersection (a1: any[], ...arrays: any[][]): any[] { - return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0)) -} - -export function difference (a1: any[], ...arrays: any[][]): any[] { - // $FlowFixMe - const a2 = [].concat.apply([], arrays) - return a1.filter(v => a2.indexOf(v) === -1) -} - -export function deepEqualJSONType (a: any, b: any): boolean { - if (a === b) return true - if (a === null || b === null || typeof (a) !== typeof (b)) return false - if (typeof a !== 'object') return a === b - if (Array.isArray(a)) { - if (a.length !== b.length) return false - } else if (a.constructor.name !== 'Object') { - throw new Error(`not JSON type: ${a}`) - } - for (const key in a) { - if (!deepEqualJSONType(a[key], b[key])) return false - } - return true -} - -/** - * Modified version of: https://github.com/component/debounce/blob/master/index.js - * Returns a function, that, as long as it continues to be invoked, will not - * be triggered. The function will be called after it stops being called for - * N milliseconds. If `immediate` is passed, trigger the function on the - * leading edge, instead of the trailing. The function also has a property 'clear' - * that is a function which will clear the timer to prevent previously scheduled executions. - * - * @source underscore.js - * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/ - * @param {Function} function to wrap - * @param {Number} timeout in ms (`100`) - * @param {Boolean} whether to execute at the beginning (`false`) - * @api public - */ -export function debounce (func: Function, wait: number, immediate: ?boolean): Function { - let timeout, args, context, timestamp, result - if (wait == null) wait = 100 - - function later () { - const last = Date.now() - timestamp - - if (last < wait && last >= 0) { - timeout = setTimeout(later, wait - last) - } else { - timeout = null - if (!immediate) { - result = func.apply(context, args) - context = args = null - } - } - } - - const debounced = function () { - context = this - args = arguments - timestamp = Date.now() - const callNow = immediate && !timeout - if (!timeout) timeout = setTimeout(later, wait) - if (callNow) { - result = func.apply(context, args) - context = args = null - } - - return result - } - - debounced.clear = function () { - if (timeout) { - clearTimeout(timeout) - timeout = null - } - } - - debounced.flush = function () { - if (timeout) { - result = func.apply(context, args) - context = args = null - clearTimeout(timeout) - timeout = null - } - } - - return debounced -} diff --git a/deno-shared/pubsub.test.ts b/deno-shared/pubsub.test.ts deleted file mode 100644 index 2cf442b6a0..0000000000 --- a/deno-shared/pubsub.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -import { createClient } from './pubsub.js' - -const should = require('should') // eslint-disable-line - -const client = createClient('ws://localhost:8080', { - manual: true, - reconnectOnDisconnection: false, - reconnectOnOnline: false, - reconnectOnTimeout: false -}) -const { - maxReconnectionDelay, - minReconnectionDelay -} = client.options - -const createRandomDelays = (number) => { - return [...new Array(number)].map((_, i) => { - client.failedConnectionAttempts = i - return client.getNextRandomDelay() - }) -} -const delays1 = createRandomDelays(10) -const delays2 = createRandomDelays(10) - -describe('Test getNextRandomDelay()', function () { - it('every delay should be longer than the previous one', function () { - // In other words, the delays should be sorted in ascending numerical order. - should(delays1).deepEqual([...delays1].sort((a, b) => a - b)) - should(delays2).deepEqual([...delays2].sort((a, b) => a - b)) - }) - - it('no delay should be shorter than the minimal reconnection delay', function () { - delays1.forEach((delay) => { - should(delay).be.greaterThanOrEqual(minReconnectionDelay) - }) - delays2.forEach((delay) => { - should(delay).be.greaterThanOrEqual(minReconnectionDelay) - }) - }) - - it('no delay should be longer than the maximal reconnection delay', function () { - delays1.forEach((delay) => { - should(delay).be.lessThanOrEqual(maxReconnectionDelay) - }) - delays2.forEach((delay) => { - should(delay).be.lessThanOrEqual(maxReconnectionDelay) - }) - }) -}) diff --git a/deno-shared/pubsub.ts b/deno-shared/pubsub.ts deleted file mode 100644 index e17a52eae6..0000000000 --- a/deno-shared/pubsub.ts +++ /dev/null @@ -1,660 +0,0 @@ -import sbp from '@sbp/sbp' -import '@sbp/okturtles.events' - -// ====== Event name constants ====== // - -export const PUBSUB_ERROR = 'pubsub-error' -export const PUBSUB_RECONNECTION_ATTEMPT = 'pubsub-reconnection-attempt' -export const PUBSUB_RECONNECTION_FAILED = 'pubsub-reconnection-failed' -export const PUBSUB_RECONNECTION_SCHEDULED = 'pubsub-reconnection-scheduled' -export const PUBSUB_RECONNECTION_SUCCEEDED = 'pubsub-reconnection-succeeded' - -// ====== Types ====== // - -/* - * Flowtype usage notes: - * - * - The '+' prefix indicates properties that should not be re-assigned or - * deleted after their initialization. - * - * - 'TimeoutID' is an opaque type declared in Flow's core definition file, - * used as the return type of the core setTimeout() function. - */ - -type Message = { - [key: string]: JSONType, - type: string -} - -type PubSubClient = { - connectionTimeoutID: TimeoutID | void, - customEventHandlers: Object, - failedConnectionAttempts: number, - isLocal: boolean, - isNew: boolean, - listeners: Object, - messageHandlers: Object, - nextConnectionAttemptDelayID: TimeoutID | void, - options: Object, - pendingSubscriptionSet: Set, - pendingSyncSet: Set, - pendingUnsubscriptionSet: Set, - pingTimeoutID: TimeoutID | void, - shouldReconnect: boolean, - socket: WebSocket | null, - subscriptionSet: Set, - url: string, - // Methods - clearAllTimers(): void, - connect(): void, - destroy(): void, - pub(contractID: string, data: JSONType): void, - scheduleConnectionAttempt(): void, - sub(contractID: string): void, - unsub(contractID: string): void -} - -type SubMessage = { - [key: string]: JSONType, - type: 'sub', - contractID: string, - dontBroadcast: boolean -} - -type UnsubMessage = { - [key: string]: JSONType, - type: 'unsub', - contractID: string, - dontBroadcast: boolean -} - -// ====== Enums ====== // - -export const NOTIFICATION_TYPE = Object.freeze({ - ENTRY: 'entry', - APP_VERSION: 'app_version', - PING: 'ping', - PONG: 'pong', - PUB: 'pub', - SUB: 'sub', - UNSUB: 'unsub' -}) - -export const REQUEST_TYPE = Object.freeze({ - PUB: 'pub', - SUB: 'sub', - UNSUB: 'unsub' -}) - -export const RESPONSE_TYPE = Object.freeze({ - ERROR: 'error', - SUCCESS: 'success' -}) - -export type NotificationTypeEnum = $Values -export type RequestTypeEnum = $Values -export type ResponseTypeEnum = $Values - -// ====== API ====== // - -/** - * Creates a pubsub client instance. - * - * @param {string} url - A WebSocket URL to connect to. - * @param {Object?} options - * {object?} handlers - Custom handlers for WebSocket events. - * {boolean?} logPingMessages - Whether to log received pings. - * {boolean?} manual - Whether the factory should call 'connect()' automatically. - * Also named 'autoConnect' or 'startClosed' in other libraries. - * {object?} messageHandlers - Custom handlers for different message types. - * {number?} pingTimeout=45_000 - How long to wait for the server to send a ping, in milliseconds. - * {boolean?} reconnectOnDisconnection=true - Whether to reconnect after a server-side disconnection. - * {boolean?} reconnectOnOnline=true - Whether to reconnect after coming back online. - * {boolean?} reconnectOnTimeout=false - Whether to reconnect after a connection timeout. - * {number?} timeout=5_000 - Connection timeout duration in milliseconds. - * @returns {PubSubClient} - */ -export function createClient (url: string, options?: Object = {}): PubSubClient { - const client: PubSubClient = { - customEventHandlers: options.handlers || {}, - // The current number of connection attempts that failed. - // Reset to 0 upon successful connection. - // Used to compute how long to wait before the next reconnection attempt. - failedConnectionAttempts: 0, - isLocal: /\/\/(localhost|127\.0\.0\.1)([:?/]|$)/.test(url), - // True if this client has never been connected yet. - isNew: true, - listeners: Object.create(null), - messageHandlers: { ...defaultMessageHandlers, ...options.messageHandlers }, - nextConnectionAttemptDelayID: undefined, - options: { ...defaultOptions, ...options }, - // Requested subscriptions for which we didn't receive a response yet. - pendingSubscriptionSet: new Set(), - pendingSyncSet: new Set(), - pendingUnsubscriptionSet: new Set(), - pingTimeoutID: undefined, - shouldReconnect: true, - // The underlying WebSocket object. - // A new one is necessary for every connection or reconnection attempt. - socket: null, - subscriptionSet: new Set(), - connectionTimeoutID: undefined, - url: url.replace(/^http/, 'ws'), - ...publicMethods - } - // Create and save references to reusable event listeners. - // Every time a new underlying WebSocket object will be created for this - // client instance, these event listeners will be detached from the older - // socket then attached to the new one, hereby avoiding both unnecessary - // allocations and garbage collections of a bunch of functions every time. - // Another benefit is the ability to patch the client protocol at runtime by - // updating the client's custom event handler map. - for (const name of Object.keys(defaultClientEventHandlers)) { - client.listeners[name] = (event) => { - try { - // Use `.call()` to pass the client via the 'this' binding. - defaultClientEventHandlers[name]?.call(client, event) - client.customEventHandlers[name]?.call(client, event) - } catch (error) { - // Do not throw any error but emit an `error` event instead. - sbp('okTurtles.events/emit', PUBSUB_ERROR, client, error.message) - } - } - } - // Add global event listeners before the first connection. - if (typeof window === 'object') { - for (const name of globalEventNames) { - window.addEventListener(name, client.listeners[name]) - } - } - if (!client.options.manual) { - client.connect() - } - return client -} - -export function createMessage (type: string, data: JSONType): string { - return JSON.stringify({ type, data }) -} - -export function createNotification (type: string, data: JSONType): string { - return JSON.stringify({ type, data }) -} - -export function createRequest (type: RequestTypeEnum, data: JSONObject, dontBroadcast: boolean = false): string { - // Had to use Object.assign() instead of object spreading to make Flow happy. - return JSON.stringify(Object.assign({ type, dontBroadcast }, data)) -} - -// These handlers receive the PubSubClient instance through the `this` binding. -const defaultClientEventHandlers = { - // Emitted when the connection is closed. - close (event: CloseEvent) { - const client = this - - console.debug('[pubsub] Event: close', event.code, event.reason) - client.failedConnectionAttempts++ - - if (client.socket) { - // Remove event listeners to avoid memory leaks. - for (const name of socketEventNames) { - client.socket.removeEventListener(name, client.listeners[name]) - } - } - client.socket = null - client.clearAllTimers() - - // See "Status Codes" https://tools.ietf.org/html/rfc6455#section-7.4 - switch (event.code) { - // TODO: verify that this list of codes is correct. - case 1000: case 1002: case 1003: case 1007: case 1008: { - client.shouldReconnect = false - break - } - default: break - } - // If we should reconnect then consider our current subscriptions as pending again, - // waiting to be restored upon reconnection. - if (client.shouldReconnect) { - client.subscriptionSet.forEach((contractID) => { - // Skip contracts from which we had to unsubscribe anyway. - if (!client.pendingUnsubscriptionSet.has(contractID)) { - client.pendingSubscriptionSet.add(contractID) - } - }) - } - // We are no longer subscribed to any contracts since we are now disconnected. - client.subscriptionSet.clear() - client.pendingUnsubscriptionSet.clear() - - if (client.shouldReconnect && client.options.reconnectOnDisconnection) { - if (client.failedConnectionAttempts > client.options.maxRetries) { - sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_FAILED, client) - } else { - // If we are definetely offline then do not try to reconnect now, - // unless the server is local. - if (!isDefinetelyOffline() || client.isLocal) { - client.scheduleConnectionAttempt() - } - } - } - }, - - // Emitted when an error has occured. - // The socket will be closed automatically by the engine if necessary. - error (event: Event) { - const client = this - // Not all error events should be logged with console.error, for example every - // failed connection attempt generates one such event. - console.warn('[pubsub] Event: error', event) - clearTimeout(client.pingTimeoutID) - }, - - // Emitted when a message is received. - // The connection will be terminated if the message is malformed or has an - // unexpected data type (e.g. binary instead of text). - message (event: MessageEvent) { - const client = this - const { data } = event - - if (typeof data !== 'string') { - sbp('okTurtles.events/emit', PUBSUB_ERROR, client, { - message: `Wrong data type: ${typeof data}` - }) - return client.destroy() - } - let msg: Message = { type: '' } - - try { - msg = messageParser(data) - } catch (error) { - sbp('okTurtles.events/emit', PUBSUB_ERROR, client, { - message: `Malformed message: ${error.message}` - }) - return client.destroy() - } - const handler = client.messageHandlers[msg.type] - - if (handler) { - handler.call(client, msg) - } else { - throw new Error(`Unhandled message type: ${msg.type}`) - } - }, - - offline (event: Event) { - console.info('[pubsub] Event: offline') - const client = this - - client.clearAllTimers() - // Reset the connection attempt counter so that we'll start a new - // reconnection loop when we are back online. - client.failedConnectionAttempts = 0 - client.socket?.close() - }, - - online (event: Event) { - console.info('[pubsub] Event: online') - const client = this - - if (client.options.reconnectOnOnline && client.shouldReconnect) { - if (!client.socket) { - client.failedConnectionAttempts = 0 - client.scheduleConnectionAttempt() - } - } - }, - - // Emitted when the connection is established. - open (event: Event) { - console.debug('[pubsub] Event: open') - const client = this - const { options } = this - - client.clearAllTimers() - sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_SUCCEEDED, client) - - // Set it to -1 so that it becomes 0 on the next `close` event. - client.failedConnectionAttempts = -1 - client.isNew = false - // Setup a ping timeout if required. - // It will close the connection if we don't get any message from the server. - if (options.pingTimeout > 0 && options.pingTimeout < Infinity) { - client.pingTimeoutID = setTimeout(() => { - client.socket?.close() - }, options.pingTimeout) - } - // We only need to handle contract resynchronization here when reconnecting. - // Not on initial connection, since the login code already does it. - if (!client.isNew) { - client.pendingSyncSet = new Set(client.pendingSubscriptionSet) - } - // Send any pending subscription request. - client.pendingSubscriptionSet.forEach((contractID) => { - client.socket?.send(createRequest(REQUEST_TYPE.SUB, { contractID }, true)) - }) - // There should be no pending unsubscription since we just got connected. - }, - - 'reconnection-attempt' (event: CustomEvent) { - console.info('[pubsub] Trying to reconnect...') - }, - - 'reconnection-succeeded' (event: CustomEvent) { - console.info('[pubsub] Connection re-established') - }, - - 'reconnection-failed' (event: CustomEvent) { - console.warn('[pubsub] Reconnection failed') - const client = this - - client.destroy() - }, - - 'reconnection-scheduled' (event: CustomEvent) { - const { delay, nth } = event.detail - console.info(`[pubsub] Scheduled connection attempt ${nth} in ~${delay} ms`) - } -} - -// These handlers receive the PubSubClient instance through the `this` binding. -const defaultMessageHandlers = { - [NOTIFICATION_TYPE.ENTRY] (msg) { - console.debug('[pubsub] Received ENTRY:', msg) - }, - - [NOTIFICATION_TYPE.PING] ({ data }) { - const client = this - - if (client.options.logPingMessages) { - console.debug(`[pubsub] Ping received in ${Date.now() - Number(data)} ms`) - } - // Reply with a pong message using the same data. - client.socket?.send(createMessage(NOTIFICATION_TYPE.PONG, data)) - // Refresh the ping timer, waiting for the next ping. - clearTimeout(client.pingTimeoutID) - client.pingTimeoutID = setTimeout(() => { - client.socket?.close() - }, client.options.pingTimeout) - }, - - // PUB can be used to send ephemeral messages outside of any contract log. - [NOTIFICATION_TYPE.PUB] (msg) { - console.debug(`[pubsub] Ignoring ${msg.type} message:`, msg.data) - }, - - [NOTIFICATION_TYPE.SUB] (msg) { - console.debug(`[pubsub] Ignoring ${msg.type} message:`, msg.data) - }, - - [NOTIFICATION_TYPE.UNSUB] (msg) { - console.debug(`[pubsub] Ignoring ${msg.type} message:`, msg.data) - }, - - [RESPONSE_TYPE.ERROR] ({ data: { type, contractID } }) { - console.warn(`[pubsub] Received ERROR response for ${type} request to ${contractID}`) - const client = this - - switch (type) { - case REQUEST_TYPE.SUB: { - console.warn(`[pubsub] Could not subscribe to ${contractID}`) - client.pendingSubscriptionSet.delete(contractID) - client.pendingSyncSet.delete(contractID) - break - } - case REQUEST_TYPE.UNSUB: { - console.warn(`[pubsub] Could not unsubscribe from ${contractID}`) - client.pendingUnsubscriptionSet.delete(contractID) - break - } - default: { - console.error(`[pubsub] Malformed response: invalid request type ${type}`) - } - } - }, - - [RESPONSE_TYPE.SUCCESS] ({ data: { type, contractID } }) { - const client = this - - switch (type) { - case REQUEST_TYPE.SUB: { - console.debug(`[pubsub] Subscribed to ${contractID}`) - client.pendingSubscriptionSet.delete(contractID) - client.subscriptionSet.add(contractID) - if (client.pendingSyncSet.has(contractID)) { - sbp('chelonia/contract/sync', contractID) - client.pendingSyncSet.delete(contractID) - } - break - } - case REQUEST_TYPE.UNSUB: { - console.debug(`[pubsub] Unsubscribed from ${contractID}`) - client.pendingUnsubscriptionSet.delete(contractID) - client.subscriptionSet.delete(contractID) - break - } - default: { - console.error(`[pubsub] Malformed response: invalid request type ${type}`) - } - } - } -} - -// TODO: verify these are good defaults -const defaultOptions = { - logPingMessages: Deno.env.get('NODE_ENV') !== 'production' && !Deno.env.get('CI'), - pingTimeout: 45000, - maxReconnectionDelay: 60000, - maxRetries: 10, - minReconnectionDelay: 500, - reconnectOnDisconnection: true, - reconnectOnOnline: true, - // Defaults to false to avoid reconnection attempts in case the server doesn't - // respond because of a failed authentication. - reconnectOnTimeout: false, - reconnectionDelayGrowFactor: 2, - timeout: 5000 -} - -const globalEventNames = ['offline', 'online'] -const socketEventNames = ['close', 'error', 'message', 'open'] - -// `navigator.onLine` can give confusing false positives when `true`, -// so we'll define `isDefinetelyOffline()` rather than `isOnline()` or `isOffline()`. -// See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine -const isDefinetelyOffline = () => typeof navigator === 'object' && navigator.onLine === false - -// Parses and validates a received message. -export const messageParser = (data: string): Message => { - const msg = JSON.parse(data) - - if (typeof msg !== 'object' || msg === null) { - throw new TypeError('Message is null or not an object') - } - const { type } = msg - - if (typeof type !== 'string' || type === '') { - throw new TypeError('Message type must be a non-empty string') - } - return msg -} - -const publicMethods = { - clearAllTimers () { - const client = this - - clearTimeout(client.connectionTimeoutID) - clearTimeout(client.nextConnectionAttemptDelayID) - clearTimeout(client.pingTimeoutID) - client.connectionTimeoutID = undefined - client.nextConnectionAttemptDelayID = undefined - client.pingTimeoutID = undefined - }, - - // Performs a connection or reconnection attempt. - connect () { - const client = this - - if (client.socket !== null) { - throw new Error('connect() can only be called if there is no current socket.') - } - if (client.nextConnectionAttemptDelayID) { - throw new Error('connect() must not be called during a reconnection delay.') - } - if (!client.shouldReconnect) { - throw new Error('connect() should no longer be called on this instance.') - } - client.socket = new WebSocket(client.url) - - if (client.options.timeout) { - client.connectionTimeoutID = setTimeout(() => { - client.connectionTimeoutID = undefined - client.socket?.close(4000, 'timeout') - }, client.options.timeout) - } - // Attach WebSocket event listeners. - for (const name of socketEventNames) { - client.socket.addEventListener(name, client.listeners[name]) - } - }, - - /** - * Immediately close the socket, stop listening for events and clear any cache. - * - * This method is used in unit tests. - * - In particular, no 'close' event handler will be called. - * - Any incoming or outgoing buffered data will be discarded. - * - Any pending messages will be discarded. - */ - destroy () { - const client = this - - client.clearAllTimers() - // Update property values. - // Note: do not clear 'client.options'. - client.pendingSubscriptionSet.clear() - client.pendingUnsubscriptionSet.clear() - client.subscriptionSet.clear() - // Remove global event listeners. - if (typeof window === 'object') { - for (const name of globalEventNames) { - window.removeEventListener(name, client.listeners[name]) - } - } - // Remove WebSocket event listeners. - if (client.socket) { - for (const name of socketEventNames) { - client.socket.removeEventListener(name, client.listeners[name]) - } - client.socket.close() - } - client.listeners = {} - client.socket = null - client.shouldReconnect = false - }, - - getNextRandomDelay (): number { - const client = this - - const { - maxReconnectionDelay, - minReconnectionDelay, - reconnectionDelayGrowFactor - } = client.options - - const minDelay = minReconnectionDelay * reconnectionDelayGrowFactor ** client.failedConnectionAttempts - const maxDelay = minDelay * reconnectionDelayGrowFactor - - return Math.min(maxReconnectionDelay, Math.round(minDelay + Math.random() * (maxDelay - minDelay))) - }, - - // Schedules a connection attempt to happen after a delay computed according to - // a randomized exponential backoff algorithm variant. - scheduleConnectionAttempt () { - const client = this - - if (!client.shouldReconnect) { - throw new Error('Cannot call `scheduleConnectionAttempt()` when `shouldReconnect` is false.') - } - if (client.nextConnectionAttemptDelayID) { - return console.warn('[pubsub] A reconnection attempt is already scheduled.') - } - const delay = client.getNextRandomDelay() - const nth = client.failedConnectionAttempts + 1 - - client.nextConnectionAttemptDelayID = setTimeout(() => { - sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_ATTEMPT, client) - client.nextConnectionAttemptDelayID = undefined - client.connect() - }, delay) - sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_SCHEDULED, client, { delay, nth }) - }, - - // Unused for now. - pub (contractID: string, data: JSONType, dontBroadcast = false) { - }, - - /** - * Sends a SUB request to the server as soon as possible. - * - * - The given contract ID will be cached until we get a relevant server - * response, allowing us to resend the same request if necessary. - * - Any identical UNSUB request that has not been sent yet will be cancelled. - * - Calling this method again before the server has responded has no effect. - * @param contractID - The ID of the contract whose updates we want to subscribe to. - */ - sub (contractID: string, dontBroadcast = false) { - const client = this - const { socket } = this - - if (!client.pendingSubscriptionSet.has(contractID)) { - client.pendingSubscriptionSet.add(contractID) - client.pendingUnsubscriptionSet.delete(contractID) - - if (socket?.readyState === WebSocket.OPEN) { - socket.send(createRequest(REQUEST_TYPE.SUB, { contractID }, dontBroadcast)) - } - } - }, - - /** - * Sends an UNSUB request to the server as soon as possible. - * - * - The given contract ID will be cached until we get a relevant server - * response, allowing us to resend the same request if necessary. - * - Any identical SUB request that has not been sent yet will be cancelled. - * - Calling this method again before the server has responded has no effect. - * @param contractID - The ID of the contract whose updates we want to unsubscribe from. - */ - unsub (contractID: string, dontBroadcast = false) { - const client = this - const { socket } = this - - if (!client.pendingUnsubscriptionSet.has(contractID)) { - client.pendingSubscriptionSet.delete(contractID) - client.pendingUnsubscriptionSet.add(contractID) - - if (socket?.readyState === WebSocket.OPEN) { - socket.send(createRequest(REQUEST_TYPE.UNSUB, { contractID }, dontBroadcast)) - } - } - } -} - -// Register custom SBP event listeners before the first connection. -for (const name of Object.keys(defaultClientEventHandlers)) { - if (name === 'error' || !socketEventNames.includes(name)) { - sbp('okTurtles.events/on', `pubsub-${name}`, (target, detail?: Object) => { - target.listeners[name]({ type: name, target, detail }) - }) - } -} - -export default { - NOTIFICATION_TYPE, - REQUEST_TYPE, - RESPONSE_TYPE, - createClient, - createMessage, - createRequest -} diff --git a/deno-shared/string.ts b/deno-shared/string.ts deleted file mode 100644 index 27eedf995d..0000000000 --- a/deno-shared/string.ts +++ /dev/null @@ -1,35 +0,0 @@ -export function dasherize (s: string): string { - return s - .trim() - .replace(/[_\s]+/g, '-') - .replace(/([A-Z])/g, '-$1') - .replace(/-+/g, '-') - .toLowerCase() -} - -export function capitalize (s: string): string { - return s.substr(0, 1).toUpperCase() + s.substring(1).toLowerCase() -} - -export function camelize (s: string): string { - return s.trim().replace(/(-|_|\s)+(.)?/g, (mathc, sep, c) => { - return c ? c.toUpperCase() : '' - }) -} - -export function startsWith (s: string, what: string): boolean { - return s.indexOf(what) === 0 -} - -export function endsWith (s: string, what: string): boolean { - const len = s.length - what.length - return len >= 0 && s.indexOf(what, len) === len -} - -export function chompLeft (s: string, what: string): string { - return s.indexOf(what) === 0 ? s.slice(what.length) : s -} - -export function chompRight (s: string, what: string): string { - return endsWith(s, what) ? s.slice(0, s.length - what.length) : s -} diff --git a/import-map.json b/import-map.json index 6592d45d78..ed71fb4eed 100644 --- a/import-map.json +++ b/import-map.json @@ -1,7 +1,7 @@ { "imports": { - "~/backend/": "./deno-backend/", - "~/shared/": "./deno-shared/", + "~/backend/": "./backend/", + "~/shared/": "./shared/", "~/": "./", "@sbp/": "https://cdn.skypack.dev/@sbp/", diff --git a/shared/domains/chelonia/GIMessage.js b/shared/domains/chelonia/GIMessage.js index 4af2f1134a..f6800f2cd1 100644 --- a/shared/domains/chelonia/GIMessage.js +++ b/shared/domains/chelonia/GIMessage.js @@ -3,50 +3,34 @@ // TODO: rename GIMessage to CMessage or something similar import { blake32Hash } from '~/shared/functions.js' -import type { JSONType, JSONObject } from '~/shared/types.js' -export type GIKeyType = '' - -export type GIKey = { - type: GIKeyType; - data: Object; // based on GIKeyType this will change - meta: Object; -} // Allows server to check if the user is allowed to register this type of contract // TODO: rename 'type' to 'contractName': -export type GIOpContract = { type: string; keyJSON: string, parentContract?: string } -export type GIOpActionEncrypted = string // encrypted version of GIOpActionUnencrypted -export type GIOpActionUnencrypted = { action: string; data: JSONType; meta: JSONObject } -export type GIOpKeyAdd = { keyHash: string, keyJSON: ?string, context: string } -export type GIOpPropSet = { key: string, value: JSONType } - -export type GIOpType = 'c' | 'ae' | 'au' | 'ka' | 'kd' | 'pu' | 'ps' | 'pd' -export type GIOpValue = GIOpContract | GIOpActionEncrypted | GIOpActionUnencrypted | GIOpKeyAdd | GIOpPropSet -export type GIOp = [GIOpType, GIOpValue] +// encrypted version of GIOpActionUnencrypted export class GIMessage { // flow type annotations to make flow happy - _decrypted: GIOpValue - _mapping: Object - _message: Object - - static OP_CONTRACT: 'c' = 'c' - static OP_ACTION_ENCRYPTED: 'ae' = 'ae' // e2e-encrypted action - static OP_ACTION_UNENCRYPTED: 'au' = 'au' // publicly readable action - static OP_KEY_ADD: 'ka' = 'ka' // add this key to the list of keys allowed to write to this contract, or update an existing key - static OP_KEY_DEL: 'kd' = 'kd' // remove this key from authorized keys - static OP_PROTOCOL_UPGRADE: 'pu' = 'pu' - static OP_PROP_SET: 'ps' = 'ps' // set a public key/value pair - static OP_PROP_DEL: 'pd' = 'pd' // delete a public key/value pair + _decrypted + _mapping + _message + + static OP_CONTRACT = 'c' + static OP_ACTION_ENCRYPTED = 'ae' // e2e-encrypted action + static OP_ACTION_UNENCRYPTED = 'au' // publicly readable action + static OP_KEY_ADD = 'ka' // add this key to the list of keys allowed to write to this contract, or update an existing key + static OP_KEY_DEL = 'kd' // remove this key from authorized keys + static OP_PROTOCOL_UPGRADE = 'pu' + static OP_PROP_SET = 'ps' // set a public key/value pair + static OP_PROP_DEL = 'pd' // delete a public key/value pair // eslint-disable-next-line camelcase static createV1_0 ( - contractID: string | null = null, - previousHEAD: string | null = null, - op: GIOp, - manifest: string, - signatureFn?: Function = defaultSignatureFn - ): this { + contractID = null, + previousHEAD = null, + op, + manifest, + signatureFn = defaultSignatureFn + ) { const message = { version: '1.0.0', previousHEAD, @@ -73,7 +57,7 @@ export class GIMessage { } // TODO: we need signature verification upon decryption somewhere... - static deserialize (value: string): this { + static deserialize (value) { if (!value) throw new Error(`deserialize bad value: ${value}`) return new this({ mapping: { key: blake32Hash(value), value }, @@ -81,7 +65,7 @@ export class GIMessage { }) } - constructor ({ mapping, message }: { mapping: Object, message: Object }) { + constructor ({ mapping, message }) { this._mapping = mapping this._message = message // perform basic sanity check @@ -98,7 +82,7 @@ export class GIMessage { } } - decryptedValue (fn?: Function): any { + decryptedValue (fn) { if (!this._decrypted) { this._decrypted = ( this.opType() === GIMessage.OP_ACTION_ENCRYPTED && fn !== undefined @@ -109,17 +93,17 @@ export class GIMessage { return this._decrypted } - message (): Object { return this._message } + message () { return this._message } - op (): GIOp { return this.message().op } + op () { return this.message().op } - opType (): GIOpType { return this.op()[0] } + opType () { return this.op()[0] } - opValue (): GIOpValue { return this.op()[1] } + opValue () { return this.op()[1] } - manifest (): string { return this.message().manifest } + manifest () { return this.message().manifest } - description (): string { + description () { const type = this.opType() let desc = `` } - isFirstMessage (): boolean { return !this.message().previousHEAD } + isFirstMessage () { return !this.message().previousHEAD } - contractID (): string { return this.message().contractID || this.hash() } + contractID () { return this.message().contractID || this.hash() } - serialize (): string { return this._mapping.value } + serialize () { return this._mapping.value } - hash (): string { return this._mapping.key } + hash () { return this._mapping.key } } -function defaultSignatureFn (data: string) { +function defaultSignatureFn (data) { return { type: 'default', sig: blake32Hash(data) diff --git a/shared/domains/chelonia/chelonia.js b/shared/domains/chelonia/chelonia.js index 7462d11dd4..1ed17b9adc 100644 --- a/shared/domains/chelonia/chelonia.js +++ b/shared/domains/chelonia/chelonia.js @@ -12,38 +12,12 @@ import { handleFetchResult } from '~/frontend/controller/utils/misc.js' // TODO: rename this to ChelMessage import { GIMessage } from './GIMessage.js' import { ChelErrorUnrecoverable } from './errors.js' -import type { GIOpContract, GIOpActionUnencrypted } from './GIMessage.js' // TODO: define ChelContractType for /defineContract -export type ChelRegParams = { - contractName: string; - server?: string; // TODO: implement! - data: Object; - hooks?: { - prepublishContract?: (GIMessage) => void; - prepublish?: (GIMessage) => void; - postpublish?: (GIMessage) => void; - }; - publishOptions?: { maxAttempts: number }; -} - -export type ChelActionParams = { - action: string; - server?: string; // TODO: implement! - contractID: string; - data: Object; - hooks?: { - prepublishContract?: (GIMessage) => void; - prepublish?: (GIMessage) => void; - postpublish?: (GIMessage) => void; - }; - publishOptions?: { maxAttempts: number }; -} - export { GIMessage } -export const ACTION_REGEX: RegExp = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ +export const ACTION_REGEX = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ // ACTION_REGEX.exec('gi.contracts/group/payment/process') // 0 => 'gi.contracts/group/payment/process' // 1 => 'gi.contracts/group/payment/' @@ -73,7 +47,7 @@ export default (sbp('sbp/selectors/register', { overrides: {}, // override default values per-contract manifests: {} // override! contract names => manifest hashes }, - whitelisted: (action: string): boolean => !!this.whitelistedActions[action], + whitelisted: (action) => !!this.whitelistedActions[action], reactiveSet: (obj, key, value) => { obj[key] = value; return value }, // example: set to Vue.set reactiveDel: (obj, key) => { delete obj[key] }, skipActionProcessing: false, @@ -100,7 +74,7 @@ export default (sbp('sbp/selectors/register', { this.manifestToContract = {} this.whitelistedActions = {} this.sideEffectStacks = {} // [contractID]: Array<*> - this.sideEffectStack = (contractID: string): Array<*> => { + this.sideEffectStack = (contractID) => { let stack = this.sideEffectStacks[contractID] if (!stack) { this.sideEffectStacks[contractID] = stack = [] @@ -111,7 +85,7 @@ export default (sbp('sbp/selectors/register', { 'chelonia/config': function () { return cloneDeep(this.config) }, - 'chelonia/configure': async function (config: Object) { + 'chelonia/configure': async function (config) { merge(this.config, config) // merge will strip the hooks off of config.hooks when merging from the root of the object // because they are functions and cloneDeep doesn't clone functions @@ -125,7 +99,7 @@ export default (sbp('sbp/selectors/register', { } }, // TODO: allow connecting to multiple servers at once - 'chelonia/connect': function (): Object { + 'chelonia/connect': function () { if (!this.config.connectionURL) throw new Error('config.connectionURL missing') if (!this.config.connectionOptions) throw new Error('config.connectionOptions missing') if (this.pubsub) { @@ -164,7 +138,7 @@ export default (sbp('sbp/selectors/register', { } return this.pubsub }, - 'chelonia/defineContract': function (contract: Object) { + 'chelonia/defineContract': function (contract) { if (!ACTION_REGEX.exec(contract.name)) throw new Error(`bad contract name: ${contract.name}`) if (!contract.metadata) contract.metadata = { validate () {}, create: () => ({}) } if (!contract.getters) contract.getters = {} @@ -178,7 +152,7 @@ export default (sbp('sbp/selectors/register', { [`${contract.manifest}/${contract.name}/getters`]: () => contract.getters, // 2 ways to cause sideEffects to happen: by defining a sideEffect function in the // contract, or by calling /pushSideEffect w/async SBP call. Can also do both. - [`${contract.manifest}/${contract.name}/pushSideEffect`]: (contractID: string, asyncSbpCall: Array<*>) => { + [`${contract.manifest}/${contract.name}/pushSideEffect`]: (contractID, asyncSbpCall) => { // if this version of the contract is pushing a sideEffect to a function defined by the // contract itself, make sure that it calls the same version of the sideEffect const [sel] = asyncSbpCall @@ -198,7 +172,7 @@ export default (sbp('sbp/selectors/register', { // - whatever keys should be passed in as well // base it off of the design of encryptedAction() this.defContractSelectors.push(...sbp('sbp/selectors/register', { - [`${contract.manifest}/${action}/process`]: (message: Object, state: Object) => { + [`${contract.manifest}/${action}/process`]: (message, state) => { const { meta, data, contractID } = message // TODO: optimize so that you're creating a proxy object only when needed const gProxy = gettersProxy(state, contract.getters) @@ -207,7 +181,7 @@ export default (sbp('sbp/selectors/register', { contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) contract.actions[action].process(message, { state, ...gProxy }) }, - [`${contract.manifest}/${action}/sideEffect`]: async (message: Object, state: ?Object) => { + [`${contract.manifest}/${action}/sideEffect`]: async (message, state) => { const sideEffects = this.sideEffectStack(message.contractID) while (sideEffects.length > 0) { const sideEffect = sideEffects.shift() @@ -260,7 +234,7 @@ export default (sbp('sbp/selectors/register', { } }, // resolves when all pending actions for these contractID(s) finish - 'chelonia/contract/wait': function (contractIDs?: string | string[]): Promise<*> { + 'chelonia/contract/wait': function (contractIDs) { const listOfIds = contractIDs ? (typeof contractIDs === 'string' ? [contractIDs] : contractIDs) : Object.keys(sbp(this.config.stateSelector).contracts) @@ -270,7 +244,7 @@ export default (sbp('sbp/selectors/register', { }, // 'chelonia/contract' - selectors related to injecting remote data and monitoring contracts // TODO: add an optional parameter to "retain" the contract (see #828) - 'chelonia/contract/sync': function (contractIDs: string | string[]): Promise<*> { + 'chelonia/contract/sync': function (contractIDs) { const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs return Promise.all(listOfIds.map(contractID => { // enqueue this invocation in a serial queue to ensure @@ -288,7 +262,7 @@ export default (sbp('sbp/selectors/register', { }, // TODO: implement 'chelonia/contract/release' (see #828) // safer version of removeImmediately that waits to finish processing events for contractIDs - 'chelonia/contract/remove': function (contractIDs: string | string[]): Promise<*> { + 'chelonia/contract/remove': function (contractIDs) { const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs return Promise.all(listOfIds.map(contractID => { return sbp('chelonia/queueInvocation', contractID, [ @@ -297,7 +271,7 @@ export default (sbp('sbp/selectors/register', { })) }, // Warning: avoid using this unless you know what you're doing. Prefer using /remove. - 'chelonia/contract/removeImmediately': function (contractID: string) { + 'chelonia/contract/removeImmediately': function (contractID) { const state = sbp(this.config.stateSelector) this.config.reactiveDel(state.contracts, contractID) this.config.reactiveDel(state, contractID) @@ -307,19 +281,19 @@ export default (sbp('sbp/selectors/register', { // TODO: r.body is a stream.Transform, should we use a callback to process // the events one-by-one instead of converting to giant json object? // however, note if we do that they would be processed in reverse... - 'chelonia/out/eventsSince': async function (contractID: string, since: string) { + 'chelonia/out/eventsSince': async function (contractID, since) { const events = await fetch(`${this.config.connectionURL}/eventsSince/${contractID}/${since}`) .then(handleFetchResult('json')) if (Array.isArray(events)) { return events.reverse().map(b64ToStr) } }, - 'chelonia/out/latestHash': function (contractID: string) { + 'chelonia/out/latestHash': function (contractID) { return fetch(`${this.config.connectionURL}/latestHash/${contractID}`, { cache: 'no-store' }).then(handleFetchResult('text')) }, - 'chelonia/out/eventsBefore': async function (before: string, limit: number) { + 'chelonia/out/eventsBefore': async function (before, limit) { if (limit <= 0) { console.error('[chelonia] invalid params error: "limit" needs to be positive integer') return @@ -331,19 +305,20 @@ export default (sbp('sbp/selectors/register', { return events.reverse().map(b64ToStr) } }, - 'chelonia/out/eventsBetween': async function (startHash: string, endHash: string, offset: number = 0) { + 'chelonia/out/eventsBetween': async function (startHash, endHash, offset = 0) { if (offset < 0) { console.error('[chelonia] invalid params error: "offset" needs to be positive integer or zero') return } const events = await fetch(`${this.config.connectionURL}/eventsBetween/${startHash}/${endHash}?offset=${offset}`) - .then(handleFetchResult('json')) + .then(handleFetchResult('json')).catch(console.error) + console.log('events:', events) if (Array.isArray(events)) { return events.reverse().map(b64ToStr) } }, - 'chelonia/latestContractState': async function (contractID: string) { + 'chelonia/latestContractState': async function (contractID) { const events = await sbp('chelonia/out/eventsSince', contractID, contractID) let state = {} // fast-path @@ -370,7 +345,7 @@ export default (sbp('sbp/selectors/register', { return state }, // 'chelonia/out' - selectors that send data out to the server - 'chelonia/out/registerContract': async function (params: ChelRegParams) { + 'chelonia/out/registerContract': async function (params) { const { contractName, hooks, publishOptions } = params const manifestHash = this.config.contracts.manifests[contractName] const contractInfo = this.manifestToContract[manifestHash] @@ -381,7 +356,7 @@ export default (sbp('sbp/selectors/register', { ({ type: contractName, keyJSON: 'TODO: add group public key here' - }: GIOpContract) + }) ], manifestHash ) @@ -398,10 +373,10 @@ export default (sbp('sbp/selectors/register', { }, // all of these functions will do both the creation of the GIMessage // and the sending of it via 'chelonia/private/out/publishEvent' - 'chelonia/out/actionEncrypted': function (params: ChelActionParams): Promise { + 'chelonia/out/actionEncrypted': function (params) { return outEncryptedOrUnencryptedAction.call(this, GIMessage.OP_ACTION_ENCRYPTED, params) }, - 'chelonia/out/actionUnencrypted': function (params: ChelActionParams): Promise { + 'chelonia/out/actionUnencrypted': function (params) { return outEncryptedOrUnencryptedAction.call(this, GIMessage.OP_ACTION_UNENCRYPTED, params) }, 'chelonia/out/keyAdd': async function () { @@ -419,9 +394,9 @@ export default (sbp('sbp/selectors/register', { 'chelonia/out/propDel': async function () { } -}): string[]) +})) -function contractNameFromAction (action: string): string { +function contractNameFromAction (action) { const regexResult = ACTION_REGEX.exec(action) const contractName = regexResult && regexResult[2] if (!contractName) throw new Error(`Poorly named action '${action}': missing contract name.`) @@ -429,8 +404,8 @@ function contractNameFromAction (action: string): string { } async function outEncryptedOrUnencryptedAction ( - opType: 'ae' | 'au', - params: ChelActionParams + opType, + params ) { const { action, contractID, data, hooks, publishOptions } = params const contractName = contractNameFromAction(action) @@ -442,7 +417,7 @@ async function outEncryptedOrUnencryptedAction ( const gProxy = gettersProxy(state, contract.getters) contract.metadata.validate(meta, { state, ...gProxy, contractID }) contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) - const unencMessage = ({ action, data, meta }: GIOpActionUnencrypted) + const unencMessage = ({ action, data, meta }) const message = GIMessage.createV1_0(contractID, previousHEAD, [ opType, @@ -463,7 +438,7 @@ async function outEncryptedOrUnencryptedAction ( // The only way to pass in the state is by creating a Proxy object that does // that for us. This allows us to maintain compatibility with Vue.js and integrate // the contract getters into the Vue-facing getters. -function gettersProxy (state: Object, getters: Object) { +function gettersProxy (state, getters) { const proxyGetters = new Proxy({}, { get (target, prop) { return getters[prop](state, proxyGetters) diff --git a/shared/domains/chelonia/db.js b/shared/domains/chelonia/db.js index 68818fa86f..8afd73e7fe 100644 --- a/shared/domains/chelonia/db.js +++ b/shared/domains/chelonia/db.js @@ -15,46 +15,46 @@ sbp('sbp/selectors/unsafe', ['chelonia/db/get', 'chelonia/db/set', 'chelonia/db/ const dbPrimitiveSelectors = process.env.LIGHTWEIGHT_CLIENT === 'true' ? { - 'chelonia/db/get': function (key): Promise<*> { + 'chelonia/db/get': function (key) { const id = sbp('chelonia/db/contractIdFromLogHEAD', key) return Promise.resolve(id ? sbp(this.config.stateSelector).contracts[id]?.HEAD : null) }, - 'chelonia/db/set': function (key, value): Promise { return Promise.resolve(value) }, - 'chelonia/db/delete': function (): Promise { return Promise.resolve() } + 'chelonia/db/set': function (key, value) { return Promise.resolve(value) }, + 'chelonia/db/delete': function () { return Promise.resolve() } } : { - 'chelonia/db/get': function (key: string): Promise<*> { + 'chelonia/db/get': function (key) { return Promise.resolve(sbp('okTurtles.data/get', key)) }, - 'chelonia/db/set': function (key: string, value: string): Promise { + 'chelonia/db/set': function (key, value) { return Promise.resolve(sbp('okTurtles.data/set', key, value)) }, - 'chelonia/db/delete': function (key: string): Promise { + 'chelonia/db/delete': function (key) { return Promise.resolve(sbp('okTurtles.data/delete', key)) } } export default (sbp('sbp/selectors/register', { ...dbPrimitiveSelectors, - 'chelonia/db/logHEAD': function (contractID: string): string { + 'chelonia/db/logHEAD': function (contractID) { return `${contractID}${headSuffix}` }, - 'chelonia/db/contractIdFromLogHEAD': function (key: string): ?string { + 'chelonia/db/contractIdFromLogHEAD': function (key) { return key.endsWith(headSuffix) ? key.slice(0, -headSuffix.length) : null }, - 'chelonia/db/latestHash': function (contractID: string): Promise { + 'chelonia/db/latestHash': function (contractID) { return sbp('chelonia/db/get', sbp('chelonia/db/logHEAD', contractID)) }, - 'chelonia/db/getEntry': async function (hash: string): Promise { + 'chelonia/db/getEntry': async function (hash) { try { - const value: string = await sbp('chelonia/db/get', hash) + const value = await sbp('chelonia/db/get', hash) if (!value) throw new Error(`no entry for ${hash}!`) return GIMessage.deserialize(value) } catch (e) { throw new ChelErrorDBConnection(`${e.name} during getEntry: ${e.message}`) } }, - 'chelonia/db/addEntry': function (entry: GIMessage): Promise { + 'chelonia/db/addEntry': function (entry) { // because addEntry contains multiple awaits - we want to make sure it gets executed // "atomically" to minimize the chance of a contract fork return sbp('okTurtles.eventQueue/queueEvent', `chelonia/db/${entry.contractID()}`, [ @@ -62,10 +62,10 @@ export default (sbp('sbp/selectors/register', { ]) }, // NEVER call this directly yourself! _always_ call 'chelonia/db/addEntry' instead - 'chelonia/private/db/addEntry': async function (entry: GIMessage): Promise { + 'chelonia/private/db/addEntry': async function (entry) { try { const { previousHEAD } = entry.message() - const contractID: string = entry.contractID() + const contractID = entry.contractID() if (await sbp('chelonia/db/get', entry.hash())) { console.warn(`[chelonia.db] entry exists: ${entry.hash()}`) return entry.hash() @@ -86,7 +86,7 @@ export default (sbp('sbp/selectors/register', { throw new ChelErrorDBConnection(`${e.name} during addEntry: ${e.message}`) } }, - 'chelonia/db/lastEntry': async function (contractID: string): Promise { + 'chelonia/db/lastEntry': async function (contractID) { try { const hash = await sbp('chelonia/db/latestHash', contractID) if (!hash) throw new Error(`contract ${contractID} has no latest hash!`) @@ -95,4 +95,4 @@ export default (sbp('sbp/selectors/register', { throw new ChelErrorDBConnection(`${e.name} during lastEntry: ${e.message}`) } } -}): any) +})) diff --git a/shared/domains/chelonia/errors.js b/shared/domains/chelonia/errors.js index be14c18384..138c91a957 100644 --- a/shared/domains/chelonia/errors.js +++ b/shared/domains/chelonia/errors.js @@ -3,7 +3,7 @@ export class ChelErrorDBBadPreviousHEAD extends Error { // ugly boilerplate because JavaScript is stupid // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types - constructor (...params: any[]) { + constructor (...params) { super(...params) // this.name = this.constructor.name this.name = 'ChelErrorDBBadPreviousHEAD' // string literal so minifier doesn't overwrite @@ -13,7 +13,7 @@ export class ChelErrorDBBadPreviousHEAD extends Error { } } export class ChelErrorDBConnection extends Error { - constructor (...params: any[]) { + constructor (...params) { super(...params) this.name = 'ChelErrorDBConnection' if (Error.captureStackTrace) { @@ -23,7 +23,7 @@ export class ChelErrorDBConnection extends Error { } export class ChelErrorUnexpected extends Error { - constructor (...params: any[]) { + constructor (...params) { super(...params) this.name = 'ChelErrorUnexpected' if (Error.captureStackTrace) { @@ -33,7 +33,7 @@ export class ChelErrorUnexpected extends Error { } export class ChelErrorUnrecoverable extends Error { - constructor (...params: any[]) { + constructor (...params) { super(...params) this.name = 'ChelErrorUnrecoverable' if (Error.captureStackTrace) { diff --git a/shared/domains/chelonia/internals.js b/shared/domains/chelonia/internals.js index 77800b31a1..a5b09b70bc 100644 --- a/shared/domains/chelonia/internals.js +++ b/shared/domains/chelonia/internals.js @@ -10,8 +10,6 @@ import { handleFetchResult } from '~/frontend/controller/utils/misc.js' import { blake32Hash } from '~/shared/functions.js' // import 'ses' -import type { GIOpContract, GIOpType, GIOpActionEncrypted, GIOpActionUnencrypted, GIOpPropSet, GIOpKeyAdd } from './GIMessage.js' - // export const FERAL_FUNCTION = Function export default (sbp('sbp/selectors/register', { @@ -19,7 +17,7 @@ export default (sbp('sbp/selectors/register', { 'chelonia/private/state': function () { return this.state }, - 'chelonia/private/loadManifest': async function (manifestHash: string) { + 'chelonia/private/loadManifest': async function (manifestHash) { if (this.manifestToContract[manifestHash]) { console.warn('[chelonia]: already loaded manifest', manifestHash) return @@ -41,8 +39,8 @@ export default (sbp('sbp/selectors/register', { .reduce(reduceAllow, {}) const allowedDoms = this.config.contracts.defaults.allowedDomains .reduce(reduceAllow, {}) - let contractName: string // eslint-disable-line prefer-const - const contractSBP = (selector: string, ...args) => { + let contractName // eslint-disable-line prefer-const + const contractSBP = (selector, ...args) => { const domain = domainFromSelector(selector) if (selector.startsWith(contractName)) { selector = `${manifestHash}/${selector}` @@ -55,7 +53,7 @@ export default (sbp('sbp/selectors/register', { } // const saferEval: Function = new FERAL_FUNCTION(` // eslint-disable-next-line no-new-func - const saferEval: Function = new Function(` + const saferEval = new Function(` return function (globals) { // almost a real sandbox // stops (() => this)().fetch @@ -119,7 +117,7 @@ export default (sbp('sbp/selectors/register', { }, // used by, e.g. 'chelonia/contract/wait' 'chelonia/private/noop': function () {}, - 'chelonia/private/out/publishEvent': async function (entry: GIMessage, { maxAttempts = 3 } = {}) { + 'chelonia/private/out/publishEvent': async function (entry, { maxAttempts = 3 } = {}) { const contractID = entry.contractID() let attempt = 1 // auto resend after short random delay @@ -158,27 +156,27 @@ export default (sbp('sbp/selectors/register', { } } }, - 'chelonia/private/in/processMessage': async function (message: GIMessage, state: Object) { + 'chelonia/private/in/processMessage': async function (message, state) { const [opT, opV] = message.op() const hash = message.hash() const contractID = message.contractID() const manifestHash = message.manifest() const config = this.config if (!state._vm) state._vm = {} - const opFns: { [GIOpType]: (any) => void } = { - [GIMessage.OP_CONTRACT] (v: GIOpContract) { + const opFns = { + [GIMessage.OP_CONTRACT] (v) { // TODO: shouldn't each contract have its own set of authorized keys? if (!state._vm.authorizedKeys) state._vm.authorizedKeys = [] // TODO: we probably want to be pushing the de-JSON-ified key here state._vm.authorizedKeys.push({ key: v.keyJSON, context: 'owner' }) }, - [GIMessage.OP_ACTION_ENCRYPTED] (v: GIOpActionEncrypted) { + [GIMessage.OP_ACTION_ENCRYPTED] (v) { if (!config.skipActionProcessing) { const decrypted = message.decryptedValue(config.decryptFn) opFns[GIMessage.OP_ACTION_UNENCRYPTED](decrypted) } }, - [GIMessage.OP_ACTION_UNENCRYPTED] (v: GIOpActionUnencrypted) { + [GIMessage.OP_ACTION_UNENCRYPTED] (v) { if (!config.skipActionProcessing) { const { data, meta, action } = v if (!config.whitelisted(action)) { @@ -188,11 +186,11 @@ export default (sbp('sbp/selectors/register', { } }, [GIMessage.OP_PROP_DEL]: notImplemented, - [GIMessage.OP_PROP_SET] (v: GIOpPropSet) { + [GIMessage.OP_PROP_SET] (v) { if (!state._vm.props) state._vm.props = {} state._vm.props[v.key] = v.value }, - [GIMessage.OP_KEY_ADD] (v: GIOpKeyAdd) { + [GIMessage.OP_KEY_ADD] (v) { // TODO: implement this. consider creating a function so that // we're not duplicating code in [GIMessage.OP_CONTRACT] // if (!state._vm.authorizedKeys) state._vm.authorizedKeys = [] @@ -217,7 +215,7 @@ export default (sbp('sbp/selectors/register', { config[`postOp_${opT}`] && config[`postOp_${opT}`](message, state) } }, - 'chelonia/private/in/enqueueHandleEvent': function (event: GIMessage) { + 'chelonia/private/in/enqueueHandleEvent': function (event) { // make sure handleEvent is called AFTER any currently-running invocations // to 'chelonia/contract/sync', to prevent gi.db from throwing // "bad previousHEAD" errors @@ -225,7 +223,7 @@ export default (sbp('sbp/selectors/register', { 'chelonia/private/in/handleEvent', event ]) }, - 'chelonia/private/in/syncContract': async function (contractID: string) { + 'chelonia/private/in/syncContract': async function (contractID) { const state = sbp(this.config.stateSelector) const latest = await sbp('chelonia/out/latestHash', contractID) console.debug(`[chelonia] syncContract: ${contractID} latestHash is: ${latest}`) @@ -263,7 +261,7 @@ export default (sbp('sbp/selectors/register', { throw e } }, - 'chelonia/private/in/handleEvent': async function (message: GIMessage) { + 'chelonia/private/in/handleEvent': async function (message) { const state = sbp(this.config.stateSelector) const contractID = message.contractID() const hash = message.hash() @@ -329,13 +327,13 @@ export default (sbp('sbp/selectors/register', { throw e } } -}): string[]) +})) const eventsToReinjest = [] const reprocessDebounced = debounce((contractID) => sbp('chelonia/contract/sync', contractID), 1000) const handleEvent = { - async addMessageToDB (message: GIMessage) { + async addMessageToDB (message) { const contractID = message.contractID() const hash = message.hash() try { @@ -364,12 +362,12 @@ const handleEvent = { throw e } }, - async processMutation (message: GIMessage, state: Object) { + async processMutation (message, state) { const contractID = message.contractID() if (message.isFirstMessage()) { // Flow doesn't understand that a first message must be a contract, // so we have to help it a bit in order to acces the 'type' property. - const { type } = ((message.opValue(): any): GIOpContract) + const { type } = ((message.opValue())) if (!state[contractID]) { console.debug(`contract ${type} registered for ${contractID}`) this.config.reactiveSet(state, contractID, {}) @@ -382,7 +380,7 @@ const handleEvent = { } await sbp('chelonia/private/in/processMessage', message, state[contractID]) }, - async processSideEffects (message: GIMessage) { + async processSideEffects (message) { if ([GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_ACTION_UNENCRYPTED].includes(message.opType())) { const contractID = message.contractID() const manifestHash = message.manifest() diff --git a/shared/functions.js b/shared/functions.js index 25886d5501..683e50c36f 100644 --- a/shared/functions.js +++ b/shared/functions.js @@ -4,14 +4,15 @@ import multihash from 'multihashes' import nacl from 'tweetnacl' import blake from 'blakejs' -// Makes the `Buffer` global available in the browser if needed. -if (typeof window === 'object' && typeof Buffer === 'undefined') { - // Only import `Buffer` to hopefully help treeshaking. - const { Buffer } = require('buffer') +import { Buffer } from 'buffer' + +if (typeof window === 'object') { window.Buffer = Buffer +} else { + global.Buffer = Buffer } -export function blake32Hash (data: string | Buffer | Uint8Array): string { +export function blake32Hash (data) { // TODO: for node/electron, switch to: https://github.com/ludios/node-blake2 const uint8array = blake.blake2b(data, null, 32) // TODO: if we switch to webpack we may need: https://github.com/feross/buffer @@ -26,18 +27,18 @@ export function blake32Hash (data: string | Buffer | Uint8Array): string { // and you have to jump through some hoops to get it to work: // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa#Unicode_strings // These hoops might result in inconsistencies between Node.js and the frontend. -export const b64ToBuf = (b64: string): Buffer => Buffer.from(b64, 'base64') -export const b64ToStr = (b64: string): string => b64ToBuf(b64).toString('utf8') -export const bufToB64 = (buf: Buffer): string => Buffer.from(buf).toString('base64') -export const strToBuf = (str: string): Buffer => Buffer.from(str, 'utf8') -export const strToB64 = (str: string): string => strToBuf(str).toString('base64') -export const bytesToB64 = (ary: Uint8Array): string => Buffer.from(ary).toString('base64') +export const b64ToBuf = (b64) => Buffer.from(b64, 'base64') +export const b64ToStr = (b64) => b64ToBuf(b64).toString('utf8') +export const bufToB64 = (buf) => Buffer.from(buf).toString('base64') +export const strToBuf = (str) => Buffer.from(str, 'utf8') +export const strToB64 = (str) => strToBuf(str).toString('base64') +export const bytesToB64 = (ary) => Buffer.from(ary).toString('base64') export function sign ( - { publicKey, secretKey }: {publicKey: string, secretKey: string}, - msg: string = 'hello!', - futz: string = '' -): string { + { publicKey, secretKey }, + msg = 'hello!', + futz = '' +) { return strToB64(JSON.stringify({ msg: msg + futz, key: publicKey, @@ -46,7 +47,7 @@ export function sign ( } export function verify ( - msg: string, key: string, sig: string -): any { + msg, key, sig +) { return nacl.sign.detached.verify(strToBuf(msg), b64ToBuf(sig), b64ToBuf(key)) } diff --git a/shared/pubsub.js b/shared/pubsub.js index cff6417f62..000170c902 100644 --- a/shared/pubsub.js +++ b/shared/pubsub.js @@ -2,7 +2,6 @@ import sbp from '@sbp/sbp' import '@sbp/okturtles.events' -import type { JSONObject, JSONType } from '~/shared/types.js' // ====== Event name constants ====== // @@ -24,53 +23,6 @@ export const PUBSUB_RECONNECTION_SUCCEEDED = 'pubsub-reconnection-succeeded' * used as the return type of the core setTimeout() function. */ -export type Message = { - [key: string]: JSONType, - +type: string -} - -export type PubSubClient = { - connectionTimeoutID: TimeoutID | void, - +customEventHandlers: Object, - failedConnectionAttempts: number, - +isLocal: boolean, - isNew: boolean, - +listeners: Object, - +messageHandlers: Object, - nextConnectionAttemptDelayID: TimeoutID | void, - +options: Object, - +pendingSubscriptionSet: Set, - pendingSyncSet: Set, - +pendingUnsubscriptionSet: Set, - pingTimeoutID: TimeoutID | void, - shouldReconnect: boolean, - socket: WebSocket | null, - +subscriptionSet: Set, - +url: string, - // Methods - clearAllTimers(): void, - connect(): void, - destroy(): void, - pub(contractID: string, data: JSONType): void, - scheduleConnectionAttempt(): void, - sub(contractID: string): void, - unsub(contractID: string): void -} - -export type SubMessage = { - [key: string]: JSONType, - +type: 'sub', - +contractID: string, - +dontBroadcast: boolean -} - -export type UnsubMessage = { - [key: string]: JSONType, - +type: 'unsub', - +contractID: string, - +dontBroadcast: boolean -} - // ====== Enums ====== // export const NOTIFICATION_TYPE = Object.freeze({ @@ -94,10 +46,6 @@ export const RESPONSE_TYPE = Object.freeze({ SUCCESS: 'success' }) -export type NotificationTypeEnum = $Values -export type RequestTypeEnum = $Values -export type ResponseTypeEnum = $Values - // ====== API ====== // /** @@ -117,8 +65,8 @@ export type ResponseTypeEnum = $Values * {number?} timeout=5_000 - Connection timeout duration in milliseconds. * @returns {PubSubClient} */ -export function createClient (url: string, options?: Object = {}): PubSubClient { - const client: PubSubClient = { +export function createClient (url, options = {}) { + const client = { customEventHandlers: options.handlers || {}, // The current number of connection attempts that failed. // Reset to 0 upon successful connection. @@ -176,11 +124,11 @@ export function createClient (url: string, options?: Object = {}): PubSubClient return client } -export function createMessage (type: string, data: JSONType): string { +export function createMessage (type, data) { return JSON.stringify({ type, data }) } -export function createRequest (type: RequestTypeEnum, data: JSONObject, dontBroadcast: boolean = false): string { +export function createRequest (type, data, dontBroadcast = false) { // Had to use Object.assign() instead of object spreading to make Flow happy. return JSON.stringify(Object.assign({ type, dontBroadcast }, data)) } @@ -188,7 +136,7 @@ export function createRequest (type: RequestTypeEnum, data: JSONObject, dontBroa // These handlers receive the PubSubClient instance through the `this` binding. const defaultClientEventHandlers = { // Emitted when the connection is closed. - close (event: CloseEvent) { + close (event) { const client = this console.debug('[pubsub] Event: close', event.code, event.reason) @@ -245,7 +193,7 @@ const defaultClientEventHandlers = { // Emitted when an error has occured. // The socket will be closed automatically by the engine if necessary. - error (event: Event) { + error (event) { const client = this // Not all error events should be logged with console.error, for example every // failed connection attempt generates one such event. @@ -256,7 +204,7 @@ const defaultClientEventHandlers = { // Emitted when a message is received. // The connection will be terminated if the message is malformed or has an // unexpected data type (e.g. binary instead of text). - message (event: MessageEvent) { + message (event) { const client = this const { data } = event @@ -266,7 +214,7 @@ const defaultClientEventHandlers = { }) return client.destroy() } - let msg: Message = { type: '' } + let msg = { type: '' } try { msg = messageParser(data) @@ -285,7 +233,7 @@ const defaultClientEventHandlers = { } }, - offline (event: Event) { + offline (event) { console.info('[pubsub] Event: offline') const client = this @@ -296,7 +244,7 @@ const defaultClientEventHandlers = { client.socket?.close(4002, 'offline') }, - online (event: Event) { + online (event) { console.info('[pubsub] Event: online') const client = this @@ -309,7 +257,7 @@ const defaultClientEventHandlers = { }, // Emitted when the connection is established. - open (event: Event) { + open (event) { console.debug('[pubsub] Event: open') const client = this const { options } = this @@ -340,22 +288,22 @@ const defaultClientEventHandlers = { // There should be no pending unsubscription since we just got connected. }, - 'reconnection-attempt' (event: CustomEvent) { + 'reconnection-attempt' (event) { console.info('[pubsub] Trying to reconnect...') }, - 'reconnection-succeeded' (event: CustomEvent) { + 'reconnection-succeeded' (event) { console.info('[pubsub] Connection re-established') }, - 'reconnection-failed' (event: CustomEvent) { + 'reconnection-failed' (event) { console.warn('[pubsub] Reconnection failed') const client = this client.destroy() }, - 'reconnection-scheduled' (event: CustomEvent) { + 'reconnection-scheduled' (event) { const { delay, nth } = event.detail console.info(`[pubsub] Scheduled connection attempt ${nth} in ~${delay} ms`) } @@ -469,7 +417,7 @@ const socketEventNames = ['close', 'error', 'message', 'open'] const isDefinetelyOffline = () => typeof navigator === 'object' && navigator.onLine === false // Parses and validates a received message. -export const messageParser = (data: string): Message => { +export const messageParser = (data) => { const msg = JSON.parse(data) if (typeof msg !== 'object' || msg === null) { @@ -557,7 +505,7 @@ const publicMethods = { client.shouldReconnect = false }, - getNextRandomDelay (): number { + getNextRandomDelay () { const client = this const { @@ -595,7 +543,7 @@ const publicMethods = { }, // Unused for now. - pub (contractID: string, data: JSONType, dontBroadcast = false) { + pub (contractID, data, dontBroadcast = false) { }, /** @@ -607,7 +555,7 @@ const publicMethods = { * - Calling this method again before the server has responded has no effect. * @param contractID - The ID of the contract whose updates we want to subscribe to. */ - sub (contractID: string, dontBroadcast = false) { + sub (contractID, dontBroadcast = false) { const client = this const { socket } = this @@ -630,7 +578,7 @@ const publicMethods = { * - Calling this method again before the server has responded has no effect. * @param contractID - The ID of the contract whose updates we want to unsubscribe from. */ - unsub (contractID: string, dontBroadcast = false) { + unsub (contractID, dontBroadcast = false) { const client = this const { socket } = this @@ -648,7 +596,7 @@ const publicMethods = { // Register custom SBP event listeners before the first connection. for (const name of Object.keys(defaultClientEventHandlers)) { if (name === 'error' || !socketEventNames.includes(name)) { - sbp('okTurtles.events/on', `pubsub-${name}`, (target, detail?: Object) => { + sbp('okTurtles.events/on', `pubsub-${name}`, (target, detail) => { target.listeners[name]({ type: name, target, detail }) }) } diff --git a/shared/string.js b/shared/string.js index 2be0de3b9c..032649b229 100644 --- a/shared/string.js +++ b/shared/string.js @@ -1,6 +1,6 @@ 'use strict' -export function dasherize (s: string): string { +export function dasherize (s) { return s .trim() .replace(/[_\s]+/g, '-') @@ -9,29 +9,29 @@ export function dasherize (s: string): string { .toLowerCase() } -export function capitalize (s: string): string { +export function capitalize (s) { return s.substr(0, 1).toUpperCase() + s.substring(1).toLowerCase() } -export function camelize (s: string): string { +export function camelize (s) { return s.trim().replace(/(-|_|\s)+(.)?/g, (mathc, sep, c) => { return c ? c.toUpperCase() : '' }) } -export function startsWith (s: string, what: string): boolean { +export function startsWith (s, what) { return s.indexOf(what) === 0 } -export function endsWith (s: string, what: string): boolean { +export function endsWith (s, what) { const len = s.length - what.length return len >= 0 && s.indexOf(what, len) === len } -export function chompLeft (s: string, what: string): string { +export function chompLeft (s, what) { return s.indexOf(what) === 0 ? s.slice(what.length) : s } -export function chompRight (s: string, what: string): string { +export function chompRight (s, what) { return endsWith(s, what) ? s.slice(0, s.length - what.length) : s } diff --git a/shared/types.js b/shared/types.js index 891431fb41..ecbb505ed0 100644 --- a/shared/types.js +++ b/shared/types.js @@ -8,26 +8,7 @@ // https://flowtype.org/docs/modules.html#import-type // https://flowtype.org/docs/advanced-configuration.html -export type JSONType = string | number | boolean | null | JSONObject | JSONArray; -export type JSONObject = { [key:string]: JSONType }; -export type JSONArray = Array; -export type ResType = - | ResTypeErr | ResTypeOK | ResTypeAlready - | ResTypeSub | ResTypeUnsub | ResTypeEntry | ResTypePub -export type ResTypeErr = 'error' -export type ResTypeOK = 'success' -export type ResTypeAlready = 'already' -export type ResTypeSub = 'sub' -export type ResTypeUnsub = 'unsub' -export type ResTypePub = 'pub' -export type ResTypeEntry = 'entry' // NOTE: If Flow isn't making any sense try changing this from a type to an interface! // https://github.com/facebook/flow/issues/3041 -export type Response = { -// export interface Response { - type: ResType; - err?: string; - data?: JSONType -} diff --git a/test/backend.test.js b/test/backend.test.js index c9735afd79..60e9ffd2aa 100644 --- a/test/backend.test.js +++ b/test/backend.test.js @@ -350,7 +350,6 @@ describe('Full walkthrough', function () { console.log(`hash for ${path.basename(filepath)}: ${hash}`) form.append('hash', hash) const blob = new Blob([buffer]) - console.log(blob); form.append('data', blob, path.basename(filepath)) await fetch(`${process.env.API_URL}/file`, { method: 'POST', body: form }) .then(handleFetchResult('text')) From 35fb008852935992f5b595e0c12171679d0bc023 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 28 Aug 2022 21:04:08 +0200 Subject: [PATCH 04/43] Ignore backend/ in .flowconfig --- .flowconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.flowconfig b/.flowconfig index 97ecb40a16..bf8aeb65cc 100644 --- a/.flowconfig +++ b/.flowconfig @@ -7,6 +7,7 @@ # - https://flowtype.org/docs/objects.html # - https://flowtype.org/docs/functions.html .*/Gruntfile.js +.*/backend/.* .*/dist/.* .*/contracts/.* .*/frontend/assets/.* From bd3198262b55c7c4e2bf76865cb1bd30fa1eec51 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 28 Aug 2022 21:04:25 +0200 Subject: [PATCH 05/43] Install Deno in Travis CI --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index a78b4d07bb..1f7f472540 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,14 @@ env: branches: only: - master # https://github.com/okTurtles/group-income/issues/58 +before_install: + # https://blog.travis-ci.com/2021-02-01-rundeno + - pwd + - curl -fsSL https://deno.land/x/install/install.sh | sh + - ls -l $HOME/.deno + - export DENO_INSTALL="$HOME/.deno" + - export PATH="$DENO_INSTALL/bin:$PATH" + - deno run https://deno.land/std/examples/welcome.ts cache: # Caches $HOME/.npm when npm ci is default script command # Caches node_modules in all other cases From bd116c5452b8b579d69146379f6e349f8d18bfb0 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 28 Aug 2022 21:09:44 +0200 Subject: [PATCH 06/43] Restore shared/types.js --- shared/types.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/shared/types.js b/shared/types.js index ecbb505ed0..891431fb41 100644 --- a/shared/types.js +++ b/shared/types.js @@ -8,7 +8,26 @@ // https://flowtype.org/docs/modules.html#import-type // https://flowtype.org/docs/advanced-configuration.html +export type JSONType = string | number | boolean | null | JSONObject | JSONArray; +export type JSONObject = { [key:string]: JSONType }; +export type JSONArray = Array; +export type ResType = + | ResTypeErr | ResTypeOK | ResTypeAlready + | ResTypeSub | ResTypeUnsub | ResTypeEntry | ResTypePub +export type ResTypeErr = 'error' +export type ResTypeOK = 'success' +export type ResTypeAlready = 'already' +export type ResTypeSub = 'sub' +export type ResTypeUnsub = 'unsub' +export type ResTypePub = 'pub' +export type ResTypeEntry = 'entry' // NOTE: If Flow isn't making any sense try changing this from a type to an interface! // https://github.com/facebook/flow/issues/3041 +export type Response = { +// export interface Response { + type: ResType; + err?: string; + data?: JSONType +} From e2264676b898e9fc73cad1958bd45e890d432b2e Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 28 Aug 2022 22:00:15 +0200 Subject: [PATCH 07/43] Make grunt deno task non-blocking --- Gruntfile.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 7b1f890b68..61dc70210a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -356,7 +356,6 @@ module.exports = (grunt) => { }, exec: { - deno: 'deno run --allow-env --allow-net --allow-read --allow-write --import-map=import-map.json --no-check backend/index.ts', eslint: 'node ./node_modules/eslint/bin/eslint.js --cache "**/*.{js,vue}"', flow: '"./node_modules/.bin/flow" --quiet || echo The Flow check failed!', puglint: '"./node_modules/.bin/pug-lint-vue" frontend/views', @@ -409,6 +408,10 @@ module.exports = (grunt) => { cypress[command](options).then(r => done(r.totalFailed === 0)).catch(done) }) + grunt.registerTask('deno', function () { + exec('deno run --allow-env --allow-net --allow-read --allow-write --import-map=import-map.json --no-check backend/index.ts') + }) + grunt.registerTask('pin', async function (version) { if (typeof version !== 'string') throw new Error('usage: grunt pin:') const done = this.async() @@ -437,7 +440,7 @@ module.exports = (grunt) => { grunt.registerTask('default', ['dev']) // TODO: add 'deploy' as per https://github.com/okTurtles/group-income/issues/10 - grunt.registerTask('dev', ['checkDependencies', 'exec:chelDeployAll', 'build:watch', 'exec:deno', 'keepalive']) + grunt.registerTask('dev', ['checkDependencies', 'exec:chelDeployAll', 'build:watch', 'deno', 'keepalive']) grunt.registerTask('dist', ['build']) // -------------------- @@ -501,7 +504,7 @@ module.exports = (grunt) => { ;[ [['Gruntfile.js'], [eslint]], - [['backend/**/*.ts', 'shared/**/*.js'], [eslint, 'exec:deno']], + [['backend/**/*.ts', 'shared/**/*.js'], [eslint, 'deno']], [['frontend/**/*.html'], ['copy']], [['frontend/**/*.js'], [eslint]], [['frontend/assets/{fonts,images}/**/*'], ['copy']], @@ -582,8 +585,8 @@ module.exports = (grunt) => { killKeepAlive = this.async() }) - grunt.registerTask('test', ['build', 'exec:chelDeployAll', 'exec:deno', 'exec:test', 'cypress']) - grunt.registerTask('test:unit', ['exec:deno', 'exec:test']) + grunt.registerTask('test', ['build', 'exec:chelDeployAll', 'deno', 'exec:test', 'cypress']) + grunt.registerTask('test:unit', ['deno', 'exec:test']) // ------------------------------------------------------------------------- // Process event handlers From 5713753fb9b51f8891076e57c566b477c800d127 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Mon, 29 Aug 2022 09:57:14 +0200 Subject: [PATCH 08/43] Fix route handler for cached assets --- Gruntfile.js | 50 ++++++++++++++++++++++++++++++++++++++++++++--- backend/routes.ts | 35 ++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 61dc70210a..212f0597bd 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -14,7 +14,7 @@ const util = require('util') const chalk = require('chalk') const crypto = require('crypto') -const { exec } = require('child_process') +const { exec, spawn } = require('child_process') const execP = util.promisify(exec) const { copyFile, readFile } = require('fs/promises') const fs = require('fs') @@ -376,7 +376,7 @@ module.exports = (grunt) => { // Grunt Tasks // ------------------------------------------------------------------------- - const child = null + let child = null grunt.registerTask('build', function () { const esbuild = this.flags.watch ? 'esbuild:watch' : 'esbuild' @@ -409,7 +409,51 @@ module.exports = (grunt) => { }) grunt.registerTask('deno', function () { - exec('deno run --allow-env --allow-net --allow-read --allow-write --import-map=import-map.json --no-check backend/index.ts') + const done = this.async() // Tell Grunt we're async. + const fork2 = function () { + grunt.log.writeln('backend: forking...') + child = spawn( + 'deno', + ['run', '--allow-env', '--allow-net', '--allow-read', '--allow-write', '--import-map=import-map.json', '--no-check', 'backend/index.ts'], + { + env: process.env + } + ) + child.on('error', (err) => { + if (err) { + console.error('error starting or sending message to child:', err) + process.exit(1) + } + }) + child.on('exit', (c) => { + if (c !== 0) { + grunt.log.error(`child exited with error code: ${c}`.bold) + // ^C can cause c to be null, which is an OK error. + process.exit(c || 0) + } + }) + child.stdout.on('data', (data) => { + console.log(String(data)) + }) + child.stderr.on('data', (data) => { + console.error(String(data)) + }) + child.on('close', (code) => { + console.log(`child process exited with code ${code}`) + }) + done() + } + if (child) { + grunt.log.writeln('Killing child!') + // Wait for successful shutdown to avoid EADDRINUSE errors. + child.on('message', () => { + child = null + fork2() + }) + child.send({ shutdown: 1 }) + } else { + fork2() + } }) grunt.registerTask('pin', async function (version) { diff --git a/backend/routes.ts b/backend/routes.ts index 6a59cc8f90..1b27758e7e 100644 --- a/backend/routes.ts +++ b/backend/routes.ts @@ -197,22 +197,29 @@ route.GET('/file/{hash}', async function handler (request, h) { // SPA routes -route.GET('/assets/{subpath*}', function handler (request, h) { - const { subpath } = request.params - const basename = pathlib.basename(subpath) - console.debug(`GET /assets/${subpath}`) - const base = pathlib.resolve('dist/assets') - // In the build config we told our bundler to use the `[name]-[hash]-cached` template - // to name immutable assets. This is useful because `dist/assets/` currently includes - // a few files without hash in their name. - if (basename.includes('-cached')) { +route.GET('/assets/{subpath*}', async function handler (request, h) { + try { + const { subpath } = request.params + console.log('subpath:', subpath) + const basename = pathlib.basename(subpath) + console.debug(`GET /assets/${subpath}`) + const base = pathlib.resolve('dist/assets') + console.log('base:', base) + // In the build config we told our bundler to use the `[name]-[hash]-cached` template + // to name immutable assets. This is useful because `dist/assets/` currently includes + // a few files without hash in their name. + if (basename.includes('-cached')) { + return (await h.file(pathlib.join(base, subpath))) + .header('etag', basename) + .header('cache-control', 'public,max-age=31536000,immutable') + } + // Files like `main.js` or `main.css` should be revalidated before use. Se we use the default headers. + // This should also be suitable for serving unversioned fonts and images. return h.file(pathlib.join(base, subpath)) - .header('etag', basename) - .header('cache-control', 'public,max-age=31536000,immutable') + } catch (err) { + console.error(err) + return logger(err) } - // Files like `main.js` or `main.css` should be revalidated before use. Se we use the default headers. - // This should also be suitable for serving unversioned fonts and images. - return h.file(pathlib.join(base, subpath)) }) route.GET('/app/{path*}', function handler (req, h) { From 6f5bbaec50ac0f1105a4b5feb2b71bbdaeed7cef Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Mon, 29 Aug 2022 13:03:54 +0200 Subject: [PATCH 09/43] Update Flow declarations and ignore shared files --- .flowconfig | 3 +++ shared/declarations.js | 13 ++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.flowconfig b/.flowconfig index bf8aeb65cc..f88a0e6b02 100644 --- a/.flowconfig +++ b/.flowconfig @@ -7,7 +7,10 @@ # - https://flowtype.org/docs/objects.html # - https://flowtype.org/docs/functions.html .*/Gruntfile.js +# Backend files use TypeScript instead of Flow since they run using Deno. .*/backend/.* +# Shared files must be imported by Deno backend files, so they better not contain Flowtype annotations. +.*/shared/.* .*/dist/.* .*/contracts/.* .*/frontend/assets/.* diff --git a/shared/declarations.js b/shared/declarations.js index aab4038aac..81254ce826 100644 --- a/shared/declarations.js +++ b/shared/declarations.js @@ -27,10 +27,6 @@ declare var Compartment: Function // TODO: Proper fix is to use: // https://github.com/okTurtles/group-income/issues/157 // ======================= -declare module '@hapi/boom' { declare module.exports: any } -declare module '@hapi/hapi' { declare module.exports: any } -declare module '@hapi/inert' { declare module.exports: any } -declare module '@hapi/joi' { declare module.exports: any } declare module 'blakejs' { declare module.exports: any } declare module 'buffer' { declare module.exports: any } declare module 'chalk' { declare module.exports: any } @@ -69,10 +65,17 @@ declare module '@utils/blockies.js' { declare module.exports: Object } declare module '~/frontend/model/contracts/misc/flowTyper.js' { declare module.exports: Object } declare module '~/frontend/model/contracts/shared/time.js' { declare module.exports: Object } declare module '@model/contracts/shared/time.js' { declare module.exports: Object } -// HACK: declared three files below but not sure why it's necessary +// HACK: declared some shared files below but not sure why it's necessary +declare module '~/shared/domains/chelonia/chelonia.js' { declare module.exports: any } +declare module '~/shared/domains/chelonia/GIMessage.js' { declare module.exports: Object } declare module '~/shared/domains/chelonia/events.js' { declare module.exports: Object } declare module '~/shared/domains/chelonia/errors.js' { declare module.exports: Object } declare module '~/shared/domains/chelonia/internals.js' { declare module.exports: Object } +declare module '~/shared/domains/chelonia/chelonia.js' { declare module.exports: any } +declare module '~/shared/functions.js' { declare module.exports: any } +declare module '~/shared/pubsub.js' { declare module.exports: any } +declare module '~/shared/types.js' { declare module.exports: any } + declare module '~/frontend/model/contracts/shared/giLodash.js' { declare module.exports: any } declare module '@model/contracts/shared/giLodash.js' { declare module.exports: any } declare module '@model/contracts/shared/constants.js' { declare module.exports: any } From 4a37434607d0e88d826e8aeba1a05f280d6fef07 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Mon, 29 Aug 2022 13:23:52 +0200 Subject: [PATCH 10/43] Fix dangling process issues --- Gruntfile.js | 139 +++++++++++++++++++++++++++------------------- backend/auth.ts | 7 ++- backend/routes.ts | 4 +- 3 files changed, 86 insertions(+), 64 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 212f0597bd..1724e27b6d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -66,7 +66,7 @@ const { EXPOSE_SBP = '' } = process.env -// const backendIndex = './backend/index.ts' +const backendIndex = './backend/index.ts' const distAssets = 'dist/assets' const distCSS = 'dist/assets/css' const distDir = 'dist' @@ -408,54 +408,6 @@ module.exports = (grunt) => { cypress[command](options).then(r => done(r.totalFailed === 0)).catch(done) }) - grunt.registerTask('deno', function () { - const done = this.async() // Tell Grunt we're async. - const fork2 = function () { - grunt.log.writeln('backend: forking...') - child = spawn( - 'deno', - ['run', '--allow-env', '--allow-net', '--allow-read', '--allow-write', '--import-map=import-map.json', '--no-check', 'backend/index.ts'], - { - env: process.env - } - ) - child.on('error', (err) => { - if (err) { - console.error('error starting or sending message to child:', err) - process.exit(1) - } - }) - child.on('exit', (c) => { - if (c !== 0) { - grunt.log.error(`child exited with error code: ${c}`.bold) - // ^C can cause c to be null, which is an OK error. - process.exit(c || 0) - } - }) - child.stdout.on('data', (data) => { - console.log(String(data)) - }) - child.stderr.on('data', (data) => { - console.error(String(data)) - }) - child.on('close', (code) => { - console.log(`child process exited with code ${code}`) - }) - done() - } - if (child) { - grunt.log.writeln('Killing child!') - // Wait for successful shutdown to avoid EADDRINUSE errors. - child.on('message', () => { - child = null - fork2() - }) - child.send({ shutdown: 1 }) - } else { - fork2() - } - }) - grunt.registerTask('pin', async function (version) { if (typeof version !== 'string') throw new Error('usage: grunt pin:') const done = this.async() @@ -483,8 +435,63 @@ module.exports = (grunt) => { }) grunt.registerTask('default', ['dev']) + + grunt.registerTask('deno:start', function () { + const done = this.async() // Tell Grunt we're async. + child = spawn( + 'deno', + ['run', '--allow-env', '--allow-net', '--allow-read', '--allow-write', '--import-map=import-map.json', '--no-check', backendIndex], + { + env: process.env + } + ) + child.on('error', (err) => { + if (err) { + console.error('Error starting or sending message to child:', err) + process.exit(1) + } + }) + child.on('exit', (c) => { + // ^C can cause c to be null, which is an OK error. + if (c === null) { + grunt.log.writeln('Backend process exited with null code.') + // process.exit(0) + } else if (c !== 0) { + grunt.log.error(`Backend process exited with error code: ${c}`.bold) + process.exit(c) + } else { + grunt.log.writeln('Backend process exited normally.') + } + }) + child.stdout.on('data', (data) => { + console.log(String(data)) + }) + child.stderr.on('data', (data) => { + console.error(String(data)) + }) + child.on('close', (code) => { + console.log(`Backend process closed with code ${code}`) + }) + child.on('spawn', () => { + grunt.log.writeln('Backend process spawned.') + done() + }) + }) + + grunt.registerTask('deno:stop', function () { + if (child) { + const killed = child.kill() + if (killed) { + grunt.log.writeln('Deno backend stopped.') + child = null + } else { + grunt.log.error('Failed to quit dangling child!') + } + } + }) + // TODO: add 'deploy' as per https://github.com/okTurtles/group-income/issues/10 - grunt.registerTask('dev', ['checkDependencies', 'exec:chelDeployAll', 'build:watch', 'deno', 'keepalive']) + grunt.registerTask('dev', ['checkDependencies', 'exec:chelDeployAll', 'build:watch', 'deno:start', 'keepalive']) grunt.registerTask('dist', ['build']) // -------------------- @@ -548,7 +555,7 @@ module.exports = (grunt) => { ;[ [['Gruntfile.js'], [eslint]], - [['backend/**/*.ts', 'shared/**/*.js'], [eslint, 'deno']], + [['backend/**/*.ts', 'shared/**/*.js'], [eslint, 'deno:stop', 'deno:start']], [['frontend/**/*.html'], ['copy']], [['frontend/**/*.js'], [eslint]], [['frontend/assets/{fonts,images}/**/*'], ['copy']], @@ -621,6 +628,19 @@ module.exports = (grunt) => { done() }) + // Stops the Flowtype server. + grunt.registerTask('flow:stop', function () { + const done = this.async() + exec('./node_modules/.bin/flow stop', (err, stdout, stderr) => { + if (!err) { + grunt.log.writeln('Flowtype server stopped') + } else { + grunt.log.error('Could not stop Flowtype server:', err.message) + } + done(err) + }) + }) + // eslint-disable-next-line no-unused-vars let killKeepAlive = null grunt.registerTask('keepalive', function () { @@ -629,14 +649,15 @@ module.exports = (grunt) => { killKeepAlive = this.async() }) - grunt.registerTask('test', ['build', 'exec:chelDeployAll', 'deno', 'exec:test', 'cypress']) - grunt.registerTask('test:unit', ['deno', 'exec:test']) + grunt.registerTask('test', ['build', 'exec:chelDeployAll', 'deno:start', 'exec:test', 'cypress', 'deno:stop', 'flow:stop']) + grunt.registerTask('test:unit', ['deno:start', 'exec:test', 'deno:stop']) // ------------------------------------------------------------------------- // Process event handlers // ------------------------------------------------------------------------- - process.on('exit', () => { + process.on('exit', (code, signal) => { + console.log('[node] Exiting with code:', code, 'signal:', signal) // Note: 'beforeExit' doesn't work. // In cases where 'watch' fails while child (server) is still running // we will exit and child will continue running in the background. @@ -644,12 +665,14 @@ module.exports = (grunt) => { // the PORT_SHIFT envar. If grunt-contrib-watch livereload process // cannot bind to the port for some reason, then the parent process // will exit leaving a dangling child server process. - if (child) { + if (child && !child.killed) { grunt.log.writeln('Quitting dangling child!') - child.send({ shutdown: 2 }) + child.kill() } - // Stops the Flowtype server. - exec('./node_modules/.bin/flow stop') + // Make sure to stop the Flowtype server in case `flow:stop` wasn't called. + exec('./node_modules/.bin/flow stop', () => { + grunt.log.writeln('Flowtype server stopped in process exit handler') + }) }) process.on('uncaughtException', (err) => { diff --git a/backend/auth.ts b/backend/auth.ts index 0e4558b917..fe54c2a9cb 100644 --- a/backend/auth.ts +++ b/backend/auth.ts @@ -1,11 +1,12 @@ -// create our auth plugin. see: +/* globals Deno */ +// Create our auth plugin. See: // https://hapijs.com/tutorials/auth // https://hapijs.com/tutorials/plugins -const { AlreadyExists, BadRequest, NotFound, PermissionDenied } = Deno.errors - import { verify, b64ToStr } from '~/shared/functions.js' +const { BadRequest, PermissionDenied } = Deno.errors + export default { name: 'gi-auth', register (server: Object, opts: Object) { diff --git a/backend/routes.ts b/backend/routes.ts index 1b27758e7e..1f36b32540 100644 --- a/backend/routes.ts +++ b/backend/routes.ts @@ -200,11 +200,9 @@ route.GET('/file/{hash}', async function handler (request, h) { route.GET('/assets/{subpath*}', async function handler (request, h) { try { const { subpath } = request.params - console.log('subpath:', subpath) - const basename = pathlib.basename(subpath) console.debug(`GET /assets/${subpath}`) + const basename = pathlib.basename(subpath) const base = pathlib.resolve('dist/assets') - console.log('base:', base) // In the build config we told our bundler to use the `[name]-[hash]-cached` template // to name immutable assets. This is useful because `dist/assets/` currently includes // a few files without hash in their name. From c2a6664117e9a86d16e890fcde8702c92ce24eba Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Tue, 30 Aug 2022 08:50:16 +0200 Subject: [PATCH 11/43] Fix spurious empty lines in backend output --- Gruntfile.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 1724e27b6d..93d8622c98 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -442,7 +442,8 @@ module.exports = (grunt) => { 'deno', ['run', '--allow-env', '--allow-net', '--allow-read', '--allow-write', '--import-map=import-map.json', '--no-check', backendIndex], { - env: process.env + env: process.env, + stdio: 'inherit' } ) child.on('error', (err) => { @@ -463,12 +464,6 @@ module.exports = (grunt) => { grunt.log.writeln('Backend process exited normally.') } }) - child.stdout.on('data', (data) => { - console.log(String(data)) - }) - child.stderr.on('data', (data) => { - console.error(String(data)) - }) child.on('close', (code) => { console.log(`Backend process closed with code ${code}`) }) From 7b82ac86a93a8ef2530f58e8db6d3ae47416784d Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Tue, 30 Aug 2022 09:02:35 +0200 Subject: [PATCH 12/43] Restore pubsub pingInterval and cleanup output --- backend/pubsub.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/pubsub.ts b/backend/pubsub.ts index 377a3e5f5b..87d26cf3ee 100644 --- a/backend/pubsub.ts +++ b/backend/pubsub.ts @@ -69,7 +69,6 @@ export function createServer(options?: Object = {}) { } server.emit = (name, ...args) => { - console.log('emit:', name) const queue = server.queuesByEventName.get(name) ?? emptySet; try { for(const callback of queue) { @@ -125,7 +124,6 @@ export function createServer(options?: Object = {}) { client.activeSinceLastPing = false client.pinged = true }) - console.log('client pinged') } }) }, server.options.pingInterval) @@ -143,7 +141,7 @@ const defaultOptions = { logPingRounds: true, logPongMessages: true, maxPayload: 6 * 1024 * 1024, - pingInterval: 0 // 30000 + pingInterval: 30000 } // Internal default handlers for server events. @@ -230,7 +228,6 @@ const internalSocketEventHandlers = { let msg: Message = { type: '' } try { - console.log('data:', data) msg = messageParser(data) } catch (error) { log.error(`Malformed message: ${error.message}`) From 5a92b171565866b1f4b0a17662795b8cebbebdc3 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Wed, 31 Aug 2022 21:45:56 +0200 Subject: [PATCH 13/43] Restore lost backend logging --- Gruntfile.js | 1 - backend/pubsub.ts | 2 +- backend/server.ts | 33 +++++++++++++++++++-------------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 93d8622c98..7b00ad1d9d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -442,7 +442,6 @@ module.exports = (grunt) => { 'deno', ['run', '--allow-env', '--allow-net', '--allow-read', '--allow-write', '--import-map=import-map.json', '--no-check', backendIndex], { - env: process.env, stdio: 'inherit' } ) diff --git a/backend/pubsub.ts b/backend/pubsub.ts index 87d26cf3ee..9918ff7b92 100644 --- a/backend/pubsub.ts +++ b/backend/pubsub.ts @@ -7,7 +7,7 @@ import { import { messageParser } from '~/shared/pubsub.js' const CI = Deno.env.get('CI') -const NODE_ENV = Deno.env.get('NODE_ENV') +const NODE_ENV = Deno.env.get('NODE_ENV') ?? 'development' const emptySet = Object.freeze(new Set()) // Used to tag console output. diff --git a/backend/server.ts b/backend/server.ts index 2e692357d3..508dcc4da4 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -19,7 +19,7 @@ import { router } from './routes.ts' import { GIMessage } from '../shared/domains/chelonia/GIMessage.js' -const { version } = await import('~/package.json', { +const { default: { version }} = await import('~/package.json', { assert: { type: "json" }, }) @@ -44,7 +44,10 @@ const API_PORT = Deno.env.get('API_PORT') const API_URL = Deno.env.get('API_URL') const CI = Deno.env.get('CI') const GI_VERSION = Deno.env.get('GI_VERSION') -const NODE_ENV = Deno.env.get('NODE_ENV') +const NODE_ENV = Deno.env.get('NODE_ENV') ?? 'development' + +console.info('GI_VERSION:', GI_VERSION) +console.info('NODE_ENV:', NODE_ENV) const pubsub = createServer({ serverHandlers: { @@ -59,11 +62,9 @@ const pubsub = createServer({ const pogoServer = pogo.server({ hostname: 'localhost', port: Number.parseInt(API_PORT), - onPreResponse (response) { + onPreResponse (response, h) { try { - if (typeof response.header === 'function') { - response.header('X-Frame-Options', 'deny') - } + response.headers.set('X-Frame-Options', 'deny') } catch (err) { console.warn('could not set X-Frame-Options header:', err.message) } @@ -74,21 +75,22 @@ const pogoServer = pogo.server({ { const originalInject = pogoServer.inject.bind(pogoServer) - pogoServer.inject = (request) => { + pogoServer.inject = async (request, connInfo) => { if (isUpgradeableRequest(request)) { return pubsub.handleUpgradeableRequest(request) } else { + const response = await originalInject(request, connInfo) + // This logging code has to be put here instead of inside onPreResponse + // because it requires access to the request object. if (NODE_ENV === 'development' && !CI) { - console.debug(grey(`${request.info.remoteAddress}: ${request.toString()} --> ${request.response.status}`)) + console.debug(gray(`${connInfo.remoteAddr.hostname}: ${request.method} ${request.url} --> ${response.status}`)) } - return originalInject(request) + return response } } } pogoServer.router = router -console.log('Backend HTTP server listening:', pogoServer.options) - sbp('okTurtles.data/set', PUBSUB_INSTANCE, pubsub) sbp('okTurtles.data/set', SERVER_INSTANCE, pogoServer) @@ -109,6 +111,9 @@ sbp('sbp/selectors/register', { } }) -pogoServer.start() - .then(() => sbp('okTurtles.events/emit', SERVER_RUNNING, pogoServer)) - .catch(console.error.bind(console, 'error in server.start():')) +try { + pogoServer.start() + sbp('okTurtles.events/emit', SERVER_RUNNING, pogoServer) +} catch (err) { + console.error('error in server.start():', err.message) +} From a40841171326021d9dad40b968529a0bd0ec3adf Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Thu, 1 Sep 2022 14:45:36 +0200 Subject: [PATCH 14/43] Refactor backend/database.ts using .push() --- backend/database.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/database.ts b/backend/database.ts index a367c28bea..2abc2d82df 100644 --- a/backend/database.ts +++ b/backend/database.ts @@ -52,13 +52,13 @@ export default (sbp('sbp/selectors/register', { const entry = await sbp('chelonia/db/getEntry', currentHEAD) if (!entry) { console.error(`read(): entry ${currentHEAD} no longer exists.`) - chunks[chunks.length] = ']' + chunks.push(']') break } - if (chunks.length > 1) chunks[chunks.length] = ',' - chunks[chunks.length] = `"${strToB64(entry.serialize())}"` + if (chunks.length > 1) chunks.push(',') + chunks.push(`"${strToB64(entry.serialize())}"`) if (currentHEAD === hash) { - chunks[chunks.length] = ']' + chunks.push(']') break } else { currentHEAD = entry.message().previousHEAD @@ -80,17 +80,17 @@ export default (sbp('sbp/selectors/register', { try { while (true) { if (!currentHEAD || !limit) { - chunks[chunks.length] = ']' + chunks.push(']') break } const entry = await sbp('chelonia/db/getEntry', currentHEAD) if (!entry) { console.error(`read(): entry ${currentHEAD} no longer exists.`) - chunks[chunks.length] = ']' + chunks.push(']') break } - if (chunks.length > 1) chunks[chunks.length] = ',' - chunks[chunks.length] = `"${strToB64(entry.serialize())}"` + if (chunks.length > 1) chunks.push(',') + chunks.push(`"${strToB64(entry.serialize())}"`) currentHEAD = entry.message().previousHEAD limit-- @@ -113,11 +113,11 @@ export default (sbp('sbp/selectors/register', { const entry = await sbp('chelonia/db/getEntry', currentHEAD) if (!entry) { console.error(`read(): entry ${currentHEAD} no longer exists.`) - chunks[chunks.length] = ']' + chunks.push(']') break } - if (chunks.length > 1) chunks[chunks.length] = ',' - chunks[chunks.length] = `"${strToB64(entry.serialize())}"` + if (chunks.length > 1) chunks.push(',') + chunks.push(`"${strToB64(entry.serialize())}"`) if (currentHEAD === startHash) { isMet = true @@ -128,7 +128,7 @@ export default (sbp('sbp/selectors/register', { currentHEAD = entry.message().previousHEAD if (!currentHEAD || (isMet && !offset)) { - chunks[chunks.length] = ']' + chunks.push(']') break } } From 13382ac1641dca7cd2323847a023ae5557aff0ab Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Thu, 1 Sep 2022 14:46:42 +0200 Subject: [PATCH 15/43] Drop --no-check flag in deno:start --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 7b00ad1d9d..437c2344d7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -440,7 +440,7 @@ module.exports = (grunt) => { const done = this.async() // Tell Grunt we're async. child = spawn( 'deno', - ['run', '--allow-env', '--allow-net', '--allow-read', '--allow-write', '--import-map=import-map.json', '--no-check', backendIndex], + ['run', '--allow-env', '--allow-net', '--allow-read', '--allow-write', '--import-map=import-map.json', backendIndex], { stdio: 'inherit' } From 786a4f015e175e4b37b204ac0f49416d3df66b61 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Thu, 1 Sep 2022 18:38:19 +0200 Subject: [PATCH 16/43] Fix 'npm backend' script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d1cdfe69c5..ccb3786e81 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "description": "", "scripts": { - "backend": "npx nodemon --watch backend/ -- --require ./Gruntfile.js --require @babel/register ./backend/index.js", + "backend": "deno run --allow-env --allow-net --allow-read --allow-write --import-map=import-map.json --watch backend/index.ts", "sass-dev": "node-sass --output-style nested --source-map true -w -r -o ./dist/assets/css ./frontend/assets/style", "sass-dist": "node-sass --output-style compressed -o ./dist/assets/css ./frontend/assets/style", "cy:open": "cypress open", From 6f6bb96e2d0137bb9882548d92238e55c0dbbeb4 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sat, 10 Sep 2022 10:17:03 +0200 Subject: [PATCH 17/43] Make grunt pin run esbuild first (@SebinSong) --- Gruntfile.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 437c2344d7..08d0162228 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -408,8 +408,8 @@ module.exports = (grunt) => { cypress[command](options).then(r => done(r.totalFailed === 0)).catch(done) }) - grunt.registerTask('pin', async function (version) { - if (typeof version !== 'string') throw new Error('usage: grunt pin:') + grunt.registerTask('_pin', async function (version) { + // a task internally executed in 'pin' task below const done = this.async() const dirPath = `contracts/${version}` @@ -434,6 +434,13 @@ module.exports = (grunt) => { done() }) + grunt.registerTask('pin', function (version) { + if (typeof version !== 'string') throw new Error('usage: grunt pin:') + + grunt.task.run('build') + grunt.task.run(`_pin:${version}`) + }) + grunt.registerTask('default', ['dev']) grunt.registerTask('deno:start', function () { From 71b74d64407ec700dba3b2ffa6ece595a5684386 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 11 Sep 2022 20:50:36 +0200 Subject: [PATCH 18/43] Migrate backend tests to Deno --- .flowconfig | 3 + Gruntfile.js | 46 +- backend/auth.ts | 2 +- backend/database.ts | 6 +- backend/index.ts | 22 +- backend/pubsub.ts | 2 +- backend/routes.ts | 6 +- backend/server.ts | 2 +- frontend/controller/actions/group.js | 2 +- frontend/controller/actions/mailbox.js | 2 +- frontend/controller/backend.js | 4 +- frontend/controller/e2e/keys.js | 4 +- frontend/controller/namespace.js | 7 +- frontend/controller/utils/misc.js | 5 +- frontend/main.js | 6 +- frontend/model/contracts/shared/giLodash.js | 44 +- frontend/model/notifications/templates.js | 2 +- frontend/model/state.js | 2 +- frontend/utils/image.js | 2 +- import-map.json | 5 +- package.json | 2 + scripts/process-shim.ts | 11 + shared/declarations.js | 18 +- .../chelonia/{GIMessage.js => GIMessage.ts} | 86 +- .../chelonia/{chelonia.js => chelonia.ts} | 105 +- shared/domains/chelonia/{db.js => db.ts} | 6 +- .../domains/chelonia/{errors.js => errors.ts} | 2 - .../domains/chelonia/{events.js => events.ts} | 0 .../chelonia/{internals.js => internals.ts} | 20 +- shared/{functions.js => functions.ts} | 0 shared/pubsub.test.js | 52 - shared/pubsub.test.ts | 64 + shared/{pubsub.js => pubsub.ts} | 0 shared/{string.js => string.ts} | 14 +- shared/{types.js => types.ts} | 0 test/avatar-caching.test.js | 25 - test/avatar-caching.test.ts | 37 + test/backend.test.js | 387 ---- test/backend.test.ts | 436 ++++ test/common/common.js | 302 +++ test/common/common.js.map | 7 + test/contracts/chatroom.js | 1001 +++++++++ test/contracts/chatroom.js.map | 7 + test/contracts/group.js | 1874 +++++++++++++++++ test/contracts/group.js.map | 7 + test/contracts/identity.js | 501 +++++ test/contracts/identity.js.map | 7 + test/contracts/mailbox.js | 543 +++++ test/contracts/mailbox.js.map | 7 + test/contracts/misc/flowTyper.js | 266 +++ test/contracts/misc/flowTyper.js.map | 7 + test/contracts/shared/constants.js | 113 + test/contracts/shared/constants.js.map | 7 + test/contracts/shared/functions.js | 353 ++++ test/contracts/shared/functions.js.map | 7 + test/contracts/shared/giLodash.js | 202 ++ test/contracts/shared/giLodash.js.map | 7 + test/contracts/shared/payments/index.js | 110 + test/contracts/shared/payments/index.js.map | 7 + test/contracts/shared/voting/proposals.js | 604 ++++++ test/contracts/shared/voting/proposals.js.map | 7 + 61 files changed, 6748 insertions(+), 637 deletions(-) create mode 100644 scripts/process-shim.ts rename shared/domains/chelonia/{GIMessage.js => GIMessage.ts} (52%) rename shared/domains/chelonia/{chelonia.js => chelonia.ts} (86%) rename shared/domains/chelonia/{db.js => db.ts} (97%) rename shared/domains/chelonia/{errors.js => errors.ts} (98%) rename shared/domains/chelonia/{events.js => events.ts} (100%) rename shared/domains/chelonia/{internals.js => internals.ts} (98%) rename shared/{functions.js => functions.ts} (100%) delete mode 100644 shared/pubsub.test.js create mode 100644 shared/pubsub.test.ts rename shared/{pubsub.js => pubsub.ts} (100%) rename shared/{string.js => string.ts} (59%) rename shared/{types.js => types.ts} (100%) delete mode 100644 test/avatar-caching.test.js create mode 100644 test/avatar-caching.test.ts delete mode 100644 test/backend.test.js create mode 100644 test/backend.test.ts create mode 100644 test/common/common.js create mode 100644 test/common/common.js.map create mode 100644 test/contracts/chatroom.js create mode 100644 test/contracts/chatroom.js.map create mode 100644 test/contracts/group.js create mode 100644 test/contracts/group.js.map create mode 100644 test/contracts/identity.js create mode 100644 test/contracts/identity.js.map create mode 100644 test/contracts/mailbox.js create mode 100644 test/contracts/mailbox.js.map create mode 100644 test/contracts/misc/flowTyper.js create mode 100644 test/contracts/misc/flowTyper.js.map create mode 100644 test/contracts/shared/constants.js create mode 100644 test/contracts/shared/constants.js.map create mode 100644 test/contracts/shared/functions.js create mode 100644 test/contracts/shared/functions.js.map create mode 100644 test/contracts/shared/giLodash.js create mode 100644 test/contracts/shared/giLodash.js.map create mode 100644 test/contracts/shared/payments/index.js create mode 100644 test/contracts/shared/payments/index.js.map create mode 100644 test/contracts/shared/voting/proposals.js create mode 100644 test/contracts/shared/voting/proposals.js.map diff --git a/.flowconfig b/.flowconfig index f88a0e6b02..fd8b7c36e5 100644 --- a/.flowconfig +++ b/.flowconfig @@ -27,6 +27,9 @@ .*/test/.* .*/test/frontend.js .*.test.js +# Not using Flow because backend tests are importing it. +.*/frontend/controller/namespace.js +.*/frontend/controller/utils/misc.js [libs] ./shared/declarations.js diff --git a/Gruntfile.js b/Gruntfile.js index 08d0162228..4d024d19d3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -226,6 +226,34 @@ module.exports = (grunt) => { entryPoints: ['./frontend/controller/serviceworkers/primary.js'] } } + esbuildOptionBags.testCommons = { + ...esbuildOptionBags.default, + bundle: true, + entryPoints: ['./frontend/common/common.js'], + external: ['dompurify', 'vue'], + outdir: './test/common', + splitting: false + } + esbuildOptionBags.testContracts = { + ...esbuildOptionBags.default, + bundle: true, + entryPoints: [ + `${contractsDir}/group.js`, + `${contractsDir}/chatroom.js`, + `${contractsDir}/identity.js`, + `${contractsDir}/mailbox.js`, + // `${contractsDir}/misc/flowTyper.js`, + `${contractsDir}/shared/voting/proposals.js`, + `${contractsDir}/shared/payments/index.js`, + `${contractsDir}/shared/constants.js`, + `${contractsDir}/shared/functions.js`, + `${contractsDir}/shared/giLodash.js` + ], + external: ['dompurify', 'vue'], + outdir: './test/contracts', + splitting: false + } + esbuildOptionBags.contracts = { ...pick(clone(esbuildOptionBags.default), [ 'define', 'bundle', 'watch', 'incremental' @@ -360,14 +388,14 @@ module.exports = (grunt) => { flow: '"./node_modules/.bin/flow" --quiet || echo The Flow check failed!', puglint: '"./node_modules/.bin/pug-lint-vue" frontend/views', stylelint: 'node ./node_modules/stylelint/bin/stylelint.js --cache "frontend/assets/style/**/*.{css,sass,scss}" "frontend/views/**/*.vue"', - // Test files: - // - anything in the `/test` folder, e.g. integration tests; - // - anything that ends with `.test.js`, e.g. unit tests for SBP domains kept in the domain folder. + // Test anything that ends with `.test.js`, e.g. unit tests for SBP domains kept in the domain folder. // The `--require` flag ensures custom Babel support in our test files. test: { cmd: 'node --experimental-fetch node_modules/mocha/bin/mocha --require ./scripts/mocha-helper.js --exit -R spec --bail "./{test/,!(node_modules|ignored|dist|historical|test)/**/}*.test.js"', options: { env: process.env } }, + // Test anything in /test that ends with `.test.ts`. + testWithDeno: 'deno test --allow-env --allow-net --allow-read --allow-write --importmap=import-map.json --no-check ./test/*.ts', chelDeployAll: 'find contracts -iname "*.manifest.json" | xargs -r ./node_modules/.bin/chel deploy ./data' } }) @@ -462,7 +490,6 @@ module.exports = (grunt) => { // ^C can cause c to be null, which is an OK error. if (c === null) { grunt.log.writeln('Backend process exited with null code.') - // process.exit(0) } else if (c !== 0) { grunt.log.error(`Backend process exited with error code: ${c}`.bold) process.exit(c) @@ -527,6 +554,12 @@ module.exports = (grunt) => { const buildContractsSlim = createEsbuildTask({ ...esbuildOptionBags.contractsSlim, plugins: defaultPlugins }) + const buildTestCommons = createEsbuildTask({ + ...esbuildOptionBags.testCommons, plugins: defaultPlugins + }) + const buildTestContracts = createEsbuildTask({ + ...esbuildOptionBags.testContracts, plugins: defaultPlugins + }) // first we build the contracts since genManifestsAndDeploy depends on that // and then we build the main bundle since it depends on manifests.json @@ -537,6 +570,7 @@ module.exports = (grunt) => { .then(() => { return Promise.all([buildMain.run(), buildServiceWorkers.run()]) }) + .then(() => Promise.all([buildTestContracts.run(), buildTestCommons.run()])) .catch(error => { grunt.log.error(error.message) process.exit(1) @@ -556,7 +590,7 @@ module.exports = (grunt) => { ;[ [['Gruntfile.js'], [eslint]], - [['backend/**/*.ts', 'shared/**/*.js'], [eslint, 'deno:stop', 'deno:start']], + [['backend/**/*.ts', 'shared/**/*.ts'], [eslint, 'deno:stop', 'deno:start']], [['frontend/**/*.html'], ['copy']], [['frontend/**/*.js'], [eslint]], [['frontend/assets/{fonts,images}/**/*'], ['copy']], @@ -651,7 +685,7 @@ module.exports = (grunt) => { }) grunt.registerTask('test', ['build', 'exec:chelDeployAll', 'deno:start', 'exec:test', 'cypress', 'deno:stop', 'flow:stop']) - grunt.registerTask('test:unit', ['deno:start', 'exec:test', 'deno:stop']) + grunt.registerTask('test:unit', ['deno:start', 'exec:test', 'exec:testWithDeno', 'deno:stop']) // ------------------------------------------------------------------------- // Process event handlers diff --git a/backend/auth.ts b/backend/auth.ts index fe54c2a9cb..1576fcc9c6 100644 --- a/backend/auth.ts +++ b/backend/auth.ts @@ -3,7 +3,7 @@ // https://hapijs.com/tutorials/auth // https://hapijs.com/tutorials/plugins -import { verify, b64ToStr } from '~/shared/functions.js' +import { verify, b64ToStr } from '~/shared/functions.ts' const { BadRequest, PermissionDenied } = Deno.errors diff --git a/backend/database.ts b/backend/database.ts index 2abc2d82df..571c92127c 100644 --- a/backend/database.ts +++ b/backend/database.ts @@ -3,8 +3,8 @@ import * as pathlib from 'path' import sbp from "@sbp/sbp" import { notFound } from 'pogo/lib/bang.ts' -import '~/shared/domains/chelonia/db.js' -import { strToB64 } from '~/shared/functions.js' +import '~/shared/domains/chelonia/db.ts' +import { strToB64 } from '~/shared/functions.ts' const CI = Deno.env.get('CI') const GI_VERSION = Deno.env.get('GI_VERSION') @@ -18,7 +18,7 @@ const readTextFileAsync = Deno.readTextFile const writeFileAsync = Deno.writeFile const writeTextFileAsync = Deno.writeTextFile -const dirExists = async (pathname) => { +const dirExists = async (pathname: numer) => { try { const stats = await Deno.stat(pathname) return stats ?? stats.isDirectory() diff --git a/backend/index.ts b/backend/index.ts index a15f7c9015..9dc8eac610 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -7,10 +7,10 @@ import "@sbp/okturtles.events" import { SERVER_RUNNING } from './events.ts' import { PUBSUB_INSTANCE } from './instance-keys.ts' -const logger = window.logger = function (err) { +window.logger = function (err) { console.error(err) err.stack && console.error(err.stack) - return err // routes.js is written in a way that depends on this returning the error + return err // routes.ts is written in a way that depends on this returning the error } const process = window.process = { @@ -32,16 +32,20 @@ function logSBP (domain, selector, data) { } } -;['backend'].forEach(domain => sbp('sbp/filters/domain/add', domain, logSBP)) +['backend'].forEach(domain => sbp('sbp/filters/domain/add', domain, logSBP)) ;[].forEach(sel => sbp('sbp/filters/selector/add', sel, logSBP)) export default (new Promise((resolve, reject) => { - sbp('okTurtles.events/on', SERVER_RUNNING, function () { - console.log(bold('backend startup sequence complete.')) - resolve() - }) - // Call this after we've registered listener for `SERVER_RUNNING`. - import('./server.ts') + try { + sbp('okTurtles.events/on', SERVER_RUNNING, function () { + console.log(bold('backend startup sequence complete.')) + resolve() + }) + // Call this after we've registered listener for `SERVER_RUNNING`. + import('./server.ts') + } catch (err) { + reject(err) + } })) const shutdownFn = function (message) { diff --git a/backend/pubsub.ts b/backend/pubsub.ts index 9918ff7b92..d9cfc2c3cc 100644 --- a/backend/pubsub.ts +++ b/backend/pubsub.ts @@ -4,7 +4,7 @@ import { isWebSocketPingEvent, } from "https://deno.land/std@0.92.0/ws/mod.ts"; -import { messageParser } from '~/shared/pubsub.js' +import { messageParser } from '~/shared/pubsub.ts' const CI = Deno.env.get('CI') const NODE_ENV = Deno.env.get('NODE_ENV') ?? 'development' diff --git a/backend/routes.ts b/backend/routes.ts index 1f36b32540..3c368f8707 100644 --- a/backend/routes.ts +++ b/backend/routes.ts @@ -1,10 +1,10 @@ -/* globals logger, Deno */ +/* globals Deno */ import { bold, yellow } from 'fmt/colors.ts' import sbp from '@sbp/sbp' -import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' -import { blake32Hash } from '~/shared/functions.js' +import { GIMessage } from '~/shared/domains/chelonia/GIMessage.ts' +import { blake32Hash } from '~/shared/functions.ts' import { badRequest } from 'pogo/lib/bang.ts' import { Router } from 'pogo' diff --git a/backend/server.ts b/backend/server.ts index 508dcc4da4..eff373715c 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -17,7 +17,7 @@ import { } from '~/backend/pubsub.ts' import { router } from './routes.ts' -import { GIMessage } from '../shared/domains/chelonia/GIMessage.js' +import { GIMessage } from '../shared/domains/chelonia/GIMessage.ts' const { default: { version }} = await import('~/package.json', { assert: { type: "json" }, diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 19f3d6650f..d0507806df 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -23,7 +23,7 @@ import { dateToPeriodStamp, addTimeToDate, DAYS_MILLIS } from '@model/contracts/ import { encryptedAction } from './utils.js' import { VOTE_FOR } from '@model/contracts/shared/voting/rules.js' import type { GIActionParams } from './types.js' -import type { GIMessage } from '~/shared/domains/chelonia/chelonia.js' +import type { GIMessage } from '~/shared/domains/chelonia/chelonia.ts' export async function leaveAllChatRooms (groupContractID: string, member: string) { // let user leaves all the chatrooms before leaving group diff --git a/frontend/controller/actions/mailbox.js b/frontend/controller/actions/mailbox.js index 3ba9d5d038..88bc3f0292 100644 --- a/frontend/controller/actions/mailbox.js +++ b/frontend/controller/actions/mailbox.js @@ -3,7 +3,7 @@ import sbp from '@sbp/sbp' import { GIErrorUIRuntimeError, L, LError } from '@common/common.js' import { encryptedAction } from './utils.js' -import type { GIMessage } from '~/shared/domains/chelonia/chelonia.js' +import type { GIMessage } from '~/shared/domains/chelonia/chelonia.ts' export default (sbp('sbp/selectors/register', { 'gi.actions/mailbox/create': async function ({ diff --git a/frontend/controller/backend.js b/frontend/controller/backend.js index 58f5b90fca..c680c86e5c 100644 --- a/frontend/controller/backend.js +++ b/frontend/controller/backend.js @@ -1,9 +1,9 @@ 'use strict' -import type { JSONObject } from '~/shared/types.js' +import type { JSONObject } from '~/shared/types.ts' import sbp from '@sbp/sbp' -import { NOTIFICATION_TYPE } from '~/shared/pubsub.js' +import { NOTIFICATION_TYPE } from '~/shared/pubsub.ts' import { handleFetchResult } from './utils/misc.js' import { PUBSUB_INSTANCE } from './instance-keys.js' diff --git a/frontend/controller/e2e/keys.js b/frontend/controller/e2e/keys.js index e1ff2a4569..e76e8d2cac 100644 --- a/frontend/controller/e2e/keys.js +++ b/frontend/controller/e2e/keys.js @@ -3,11 +3,11 @@ 'use strict' import sbp from '@sbp/sbp' -import { blake32Hash, bytesToB64 } from '~/shared/functions.js' +import { blake32Hash, bytesToB64 } from '~/shared/functions.ts' import nacl from 'tweetnacl' import scrypt from 'scrypt-async' -import type { GIKey, GIKeyType } from '~/shared/domains/chelonia/GIMessage.js' +import type { GIKey, GIKeyType } from '~/shared/domains/chelonia/GIMessage.ts' function genSeed (): string { return bytesToB64(nacl.randomBytes(nacl.box.secretKeyLength)) diff --git a/frontend/controller/namespace.js b/frontend/controller/namespace.js index 2cef2eea17..07d93573af 100644 --- a/frontend/controller/namespace.js +++ b/frontend/controller/namespace.js @@ -1,3 +1,4 @@ +// @no-flow 'use strict' import sbp from '@sbp/sbp' @@ -5,7 +6,7 @@ import { handleFetchResult } from './utils/misc.js' // NOTE: prefix groups with `group/` and users with `user/` ? sbp('sbp/selectors/register', { - 'namespace/register': (name: string, value: string) => { + 'namespace/register': (name, value) => { return fetch(`${sbp('okTurtles.data/get', 'API_URL')}/name`, { method: 'POST', body: JSON.stringify({ name, value }), @@ -14,9 +15,9 @@ sbp('sbp/selectors/register', { } }).then(handleFetchResult('json')) }, - 'namespace/lookup': (name: string) => { + 'namespace/lookup': (name) => { // TODO: should `name` be encodeURI'd? - return fetch(`${sbp('okTurtles.data/get', 'API_URL')}/name/${name}`).then((r: Object) => { + return fetch(`${sbp('okTurtles.data/get', 'API_URL')}/name/${name}`).then((r) => { if (!r.ok) { console.warn(`namespace/lookup: ${r.status} for ${name}`) if (r.status !== 404) { diff --git a/frontend/controller/utils/misc.js b/frontend/controller/utils/misc.js index b59c1fbfcc..6ebddc712c 100644 --- a/frontend/controller/utils/misc.js +++ b/frontend/controller/utils/misc.js @@ -1,7 +1,8 @@ +// @noflow 'use strict' -export function handleFetchResult (type: string): ((r: any) => any) { - return function (r: Object) { +export function handleFetchResult (type) { + return function (r) { if (!r.ok) throw new Error(`${r.status}: ${r.statusText}`) return r[type]() } diff --git a/frontend/main.js b/frontend/main.js index b5c67c56c7..dee3bcee72 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -10,9 +10,9 @@ import { mapMutations, mapGetters, mapState } from 'vuex' import 'wicg-inert' import '@model/captureLogs.js' -import type { GIMessage } from '~/shared/domains/chelonia/chelonia.js' -import '~/shared/domains/chelonia/chelonia.js' -import { CONTRACT_IS_SYNCING } from '~/shared/domains/chelonia/events.js' +import type { GIMessage } from '~/shared/domains/chelonia/chelonia.ts' +import '~/shared/domains/chelonia/chelonia.ts' +import { CONTRACT_IS_SYNCING } from '~/shared/domains/chelonia/events.ts' import * as Common from '@common/common.js' import { LOGIN, LOGOUT } from './utils/events.js' import './controller/namespace.js' diff --git a/frontend/model/contracts/shared/giLodash.js b/frontend/model/contracts/shared/giLodash.js index aa255a0374..e09676bc68 100644 --- a/frontend/model/contracts/shared/giLodash.js +++ b/frontend/model/contracts/shared/giLodash.js @@ -2,22 +2,22 @@ // https://github.com/lodash/babel-plugin-lodash // additional tiny versions of lodash functions are available in VueScript2 -export function mapValues (obj: Object, fn: Function, o: Object = {}): any { +export function mapValues (obj, fn, o = {}) { for (const key in obj) { o[key] = fn(obj[key]) } return o } -export function mapObject (obj: Object, fn: Function): {[any]: any} { +export function mapObject (obj, fn) { return Object.fromEntries(Object.entries(obj).map(fn)) } -export function pick (o: Object, props: string[]): Object { +export function pick (o, props) { const x = {} for (const k of props) { x[k] = o[k] } return x } -export function pickWhere (o: Object, where: Function): Object { +export function pickWhere (o, where) { const x = {} for (const k in o) { if (where(o[k])) { x[k] = o[k] } @@ -25,13 +25,13 @@ export function pickWhere (o: Object, where: Function): Object { return x } -export function choose (array: Array<*>, indices: Array): Array<*> { +export function choose (array, indices) { const x = [] for (const idx of indices) { x.push(array[idx]) } return x } -export function omit (o: Object, props: string[]): {...} { +export function omit (o, props) { const x = {} for (const k in o) { if (!props.includes(k)) { @@ -41,7 +41,7 @@ export function omit (o: Object, props: string[]): {...} { return x } -export function cloneDeep (obj: Object): any { +export function cloneDeep (obj) { return JSON.parse(JSON.stringify(obj)) } @@ -51,7 +51,7 @@ function isMergeableObject (val) { return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]' } -export function merge (obj: Object, src: Object): any { +export function merge (obj, src) { for (const key in src) { const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined if (clone && isMergeableObject(obj[key])) { @@ -63,31 +63,31 @@ export function merge (obj: Object, src: Object): any { return obj } -export function delay (msec: number): Promise { +export function delay (msec) { return new Promise((resolve, reject) => { setTimeout(resolve, msec) }) } -export function randomBytes (length: number): Uint8Array { +export function randomBytes (length) { // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019 return crypto.getRandomValues(new Uint8Array(length)) } -export function randomHexString (length: number): string { +export function randomHexString (length) { return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('') } -export function randomIntFromRange (min: number, max: number): number { +export function randomIntFromRange (min, max) { return Math.floor(Math.random() * (max - min + 1) + min) } -export function randomFromArray (arr: any[]): any { +export function randomFromArray (arr) { return arr[Math.floor(Math.random() * arr.length)] } -export function flatten (arr: Array<*>): Array { - let flat: Array<*> = [] +export function flatten (arr) { + let flat = [] for (let i = 0; i < arr.length; i++) { if (Array.isArray(arr[i])) { flat = flat.concat(arr[i]) @@ -98,7 +98,7 @@ export function flatten (arr: Array<*>): Array { return flat } -export function zip (): any[] { +export function zip () { // $FlowFixMe const arr = Array.prototype.slice.call(arguments) const zipped = [] @@ -113,26 +113,26 @@ export function zip (): any[] { return zipped } -export function uniq (array: any[]): any[] { +export function uniq (array) { return Array.from(new Set(array)) } -export function union (...arrays: any[][]): any[] { +export function union (...arrays) { // $FlowFixMe return uniq([].concat.apply([], arrays)) } -export function intersection (a1: any[], ...arrays: any[][]): any[] { +export function intersection (a1, ...arrays) { return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0)) } -export function difference (a1: any[], ...arrays: any[][]): any[] { +export function difference (a1, ...arrays) { // $FlowFixMe const a2 = [].concat.apply([], arrays) return a1.filter(v => a2.indexOf(v) === -1) } -export function deepEqualJSONType (a: any, b: any): boolean { +export function deepEqualJSONType (a, b) { if (a === b) return true if (a === null || b === null || typeof (a) !== typeof (b)) return false if (typeof a !== 'object') return a === b @@ -162,7 +162,7 @@ export function deepEqualJSONType (a: any, b: any): boolean { * @param {Boolean} whether to execute at the beginning (`false`) * @api public */ -export function debounce (func: Function, wait: number, immediate: ?boolean): Function { +export function debounce (func, wait, immediate) { let timeout, args, context, timestamp, result if (wait == null) wait = 100 diff --git a/frontend/model/notifications/templates.js b/frontend/model/notifications/templates.js index b6f85e54d0..e55c0770dd 100644 --- a/frontend/model/notifications/templates.js +++ b/frontend/model/notifications/templates.js @@ -1,4 +1,4 @@ -import type { GIMessage } from '~/shared/domains/chelonia/chelonia.js' +import type { GIMessage } from '~/shared/domains/chelonia/chelonia.ts' import type { NewProposalType, NotificationTemplate diff --git a/frontend/model/state.js b/frontend/model/state.js index d3d02350f2..9c7c62121b 100644 --- a/frontend/model/state.js +++ b/frontend/model/state.js @@ -5,7 +5,7 @@ import sbp from '@sbp/sbp' import { Vue } from '@common/common.js' -import { EVENT_HANDLED, CONTRACT_REGISTERED } from '~/shared/domains/chelonia/events.js' +import { EVENT_HANDLED, CONTRACT_REGISTERED } from '~/shared/domains/chelonia/events.ts' import Vuex from 'vuex' import Colors from './colors.js' import { CHATROOM_PRIVACY_LEVEL } from '@model/contracts/shared/constants.js' diff --git a/frontend/utils/image.js b/frontend/utils/image.js index 2beedb895e..59bc3f187b 100644 --- a/frontend/utils/image.js +++ b/frontend/utils/image.js @@ -1,7 +1,7 @@ 'use strict' import sbp from '@sbp/sbp' -import { blake32Hash } from '~/shared/functions.js' +import { blake32Hash } from '~/shared/functions.ts' import { handleFetchResult } from '~/frontend/controller/utils/misc.js' // Copied from https://stackoverflow.com/a/27980815/4737729 diff --git a/import-map.json b/import-map.json index ed71fb4eed..6b17daaab9 100644 --- a/import-map.json +++ b/import-map.json @@ -3,6 +3,7 @@ "~/backend/": "./backend/", "~/shared/": "./shared/", "~/": "./", + "@common/": "./frontend/common/", "@sbp/": "https://cdn.skypack.dev/@sbp/", "fmt/": "https://deno.land/std@0.138.0/fmt/", @@ -10,9 +11,11 @@ "blakejs": "https://cdn.skypack.dev/blakejs", "buffer": "https://cdn.skypack.dev/buffer", + "dompurify": "https://cdn.skypack.dev/dompurify", "multihashes": "https://esm.sh/multihashes", "pogo": "https://raw.githubusercontent.com/snowteamer/pogo/master/main.ts", "pogo/": "https://raw.githubusercontent.com/snowteamer/pogo/master/", - "tweetnacl": "https://cdn.skypack.dev/tweetnacl" + "tweetnacl": "https://cdn.skypack.dev/tweetnacl", + "vue": "https://esm.sh/vue@2.7.10" } } \ No newline at end of file diff --git a/package.json b/package.json index ccb3786e81..cb3a8d7613 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,8 @@ "dist/*", "ignored/*", "node_modules/*", + "test/common/*", + "test/contracts/*", "test/cypress/cache/*", "contracts/*" ], diff --git a/scripts/process-shim.ts b/scripts/process-shim.ts new file mode 100644 index 0000000000..1e84ad93c6 --- /dev/null +++ b/scripts/process-shim.ts @@ -0,0 +1,11 @@ +window.process = { + env: { + get (key) { + return Deno.env.get(key) + }, + set (key, value) { + return Deno.env.set(key, value) + } + } +} +console.log(process); diff --git a/shared/declarations.js b/shared/declarations.js index 81254ce826..6e270b7c1e 100644 --- a/shared/declarations.js +++ b/shared/declarations.js @@ -62,18 +62,18 @@ declare module 'favico.js' { declare module.exports: any } declare module '@assets/style/main.scss' { declare module.exports: any } // Other .js files. declare module '@utils/blockies.js' { declare module.exports: Object } +declare module '~/frontend/controller/utils/misc.js' { declare module.exports: Function } declare module '~/frontend/model/contracts/misc/flowTyper.js' { declare module.exports: Object } declare module '~/frontend/model/contracts/shared/time.js' { declare module.exports: Object } declare module '@model/contracts/shared/time.js' { declare module.exports: Object } // HACK: declared some shared files below but not sure why it's necessary -declare module '~/shared/domains/chelonia/chelonia.js' { declare module.exports: any } -declare module '~/shared/domains/chelonia/GIMessage.js' { declare module.exports: Object } -declare module '~/shared/domains/chelonia/events.js' { declare module.exports: Object } -declare module '~/shared/domains/chelonia/errors.js' { declare module.exports: Object } -declare module '~/shared/domains/chelonia/internals.js' { declare module.exports: Object } -declare module '~/shared/domains/chelonia/chelonia.js' { declare module.exports: any } -declare module '~/shared/functions.js' { declare module.exports: any } -declare module '~/shared/pubsub.js' { declare module.exports: any } +declare module '~/shared/domains/chelonia/GIMessage.ts' { declare module.exports: Object } +declare module '~/shared/domains/chelonia/chelonia.ts' { declare module.exports: any } +declare module '~/shared/domains/chelonia/errors.ts' { declare module.exports: Object } +declare module '~/shared/domains/chelonia/events.ts' { declare module.exports: Object } +declare module '~/shared/domains/chelonia/internals.ts' { declare module.exports: Object } +declare module '~/shared/functions.ts' { declare module.exports: any } +declare module '~/shared/pubsub.ts' { declare module.exports: any } declare module '~/shared/types.js' { declare module.exports: any } declare module '~/frontend/model/contracts/shared/giLodash.js' { declare module.exports: any } @@ -84,4 +84,6 @@ declare module '@model/contracts/shared/voting/rules.js' { declare module.export declare module '@model/contracts/shared/voting/proposals.js' { declare module.exports: any } declare module '@model/contracts/shared/functions.js' { declare module.exports: any } declare module '@common/common.js' { declare module.exports: any } +declare module './controller/namespace.js' { declare module.exports: any } declare module './model/contracts/manifests.json' { declare module.exports: any } +declare module './utils/misc.js' { declare module.exports: any } diff --git a/shared/domains/chelonia/GIMessage.js b/shared/domains/chelonia/GIMessage.ts similarity index 52% rename from shared/domains/chelonia/GIMessage.js rename to shared/domains/chelonia/GIMessage.ts index f6800f2cd1..b5ee282a23 100644 --- a/shared/domains/chelonia/GIMessage.js +++ b/shared/domains/chelonia/GIMessage.ts @@ -1,36 +1,50 @@ -'use strict' - // TODO: rename GIMessage to CMessage or something similar -import { blake32Hash } from '~/shared/functions.js' +import { blake32Hash } from '~/shared/functions.ts' +import type { JSONType, JSONObject } from '~/shared/types.ts' + +export type GIKeyType = '' +export type GIKey = { + type: GIKeyType; + data: Object; // based on GIKeyType this will change + meta: Object; +} // Allows server to check if the user is allowed to register this type of contract // TODO: rename 'type' to 'contractName': -// encrypted version of GIOpActionUnencrypted +export type GIOpContract = { type: string; keyJSON: string, parentContract?: string } +export type GIOpActionEncrypted = string // encrypted version of GIOpActionUnencrypted +export type GIOpActionUnencrypted = { action: string; data: JSONType; meta: JSONObject } +export type GIOpKeyAdd = { keyHash: string, keyJSON: string | null | void, context: string } +export type GIOpPropSet = { key: string, value: JSONType } + +export type GIOpType = 'c' | 'ae' | 'au' | 'ka' | 'kd' | 'pu' | 'ps' | 'pd' +export type GIOpValue = GIOpContract | GIOpActionEncrypted | GIOpActionUnencrypted | GIOpKeyAdd | GIOpPropSet +export type GIOp = [GIOpType, GIOpValue] export class GIMessage { // flow type annotations to make flow happy - _decrypted - _mapping - _message - - static OP_CONTRACT = 'c' - static OP_ACTION_ENCRYPTED = 'ae' // e2e-encrypted action - static OP_ACTION_UNENCRYPTED = 'au' // publicly readable action - static OP_KEY_ADD = 'ka' // add this key to the list of keys allowed to write to this contract, or update an existing key - static OP_KEY_DEL = 'kd' // remove this key from authorized keys - static OP_PROTOCOL_UPGRADE = 'pu' - static OP_PROP_SET = 'ps' // set a public key/value pair - static OP_PROP_DEL = 'pd' // delete a public key/value pair + _decrypted: GIOpValue + _mapping: Object + _message: Object + + static OP_CONTRACT: 'c' = 'c' + static OP_ACTION_ENCRYPTED: 'ae' = 'ae' // e2e-encrypted action + static OP_ACTION_UNENCRYPTED: 'au' = 'au' // publicly readable action + static OP_KEY_ADD: 'ka' = 'ka' // add this key to the list of keys allowed to write to this contract, or update an existing key + static OP_KEY_DEL: 'kd' = 'kd' // remove this key from authorized keys + static OP_PROTOCOL_UPGRADE: 'pu' = 'pu' + static OP_PROP_SET: 'ps' = 'ps' // set a public key/value pair + static OP_PROP_DEL: 'pd' = 'pd' // delete a public key/value pair // eslint-disable-next-line camelcase static createV1_0 ( - contractID = null, - previousHEAD = null, - op, - manifest, - signatureFn = defaultSignatureFn - ) { + contractID: string | null = null, + previousHEAD: string | null = null, + op: GIOp, + manifest: string, + signatureFn?: Function = defaultSignatureFn + ): this { const message = { version: '1.0.0', previousHEAD, @@ -57,7 +71,7 @@ export class GIMessage { } // TODO: we need signature verification upon decryption somewhere... - static deserialize (value) { + static deserialize (value: string): this { if (!value) throw new Error(`deserialize bad value: ${value}`) return new this({ mapping: { key: blake32Hash(value), value }, @@ -65,7 +79,7 @@ export class GIMessage { }) } - constructor ({ mapping, message }) { + constructor ({ mapping, message }: { mapping: Object, message: Object }) { this._mapping = mapping this._message = message // perform basic sanity check @@ -82,7 +96,7 @@ export class GIMessage { } } - decryptedValue (fn) { + decryptedValue (fn?: Function): any { if (!this._decrypted) { this._decrypted = ( this.opType() === GIMessage.OP_ACTION_ENCRYPTED && fn !== undefined @@ -93,17 +107,17 @@ export class GIMessage { return this._decrypted } - message () { return this._message } + message (): Object { return this._message } - op () { return this.message().op } + op (): GIOp { return this.message().op } - opType () { return this.op()[0] } + opType (): GIOpType { return this.op()[0] } - opValue () { return this.op()[1] } + opValue (): GIOpValue { return this.op()[1] } - manifest () { return this.message().manifest } + manifest (): string { return this.message().manifest } - description () { + description (): string { const type = this.opType() let desc = `` } - isFirstMessage () { return !this.message().previousHEAD } + isFirstMessage (): boolean { return !this.message().previousHEAD } - contractID () { return this.message().contractID || this.hash() } + contractID (): string { return this.message().contractID || this.hash() } - serialize () { return this._mapping.value } + serialize (): string { return this._mapping.value } - hash () { return this._mapping.key } + hash (): string { return this._mapping.key } } -function defaultSignatureFn (data) { +function defaultSignatureFn (data: string) { return { type: 'default', sig: blake32Hash(data) diff --git a/shared/domains/chelonia/chelonia.js b/shared/domains/chelonia/chelonia.ts similarity index 86% rename from shared/domains/chelonia/chelonia.js rename to shared/domains/chelonia/chelonia.ts index 1ed17b9adc..6c4161dbdc 100644 --- a/shared/domains/chelonia/chelonia.js +++ b/shared/domains/chelonia/chelonia.ts @@ -1,23 +1,47 @@ -'use strict' - import sbp from '@sbp/sbp' import '@sbp/okturtles.events' import '@sbp/okturtles.eventqueue' -import './internals.js' -import { CONTRACTS_MODIFIED, CONTRACT_REGISTERED } from './events.js' -import { createClient, NOTIFICATION_TYPE } from '~/shared/pubsub.js' +import './internals.ts' +import { CONTRACTS_MODIFIED, CONTRACT_REGISTERED } from './events.ts' +import { createClient, NOTIFICATION_TYPE } from '~/shared/pubsub.ts' import { merge, cloneDeep, randomHexString, intersection, difference } from '~/frontend/model/contracts/shared/giLodash.js' -import { b64ToStr } from '~/shared/functions.js' +import { b64ToStr } from '~/shared/functions.ts' import { handleFetchResult } from '~/frontend/controller/utils/misc.js' // TODO: rename this to ChelMessage -import { GIMessage } from './GIMessage.js' -import { ChelErrorUnrecoverable } from './errors.js' +import { GIMessage } from './GIMessage.ts' +import { ChelErrorUnrecoverable } from './errors.ts' +import type { GIOpContract, GIOpActionUnencrypted } from './GIMessage.ts' // TODO: define ChelContractType for /defineContract +export type ChelRegParams = { + contractName: string; + server?: string; // TODO: implement! + data: Object; + hooks?: { + prepublishContract?: (GIMessage) => void; + prepublish?: (GIMessage) => void; + postpublish?: (GIMessage) => void; + }; + publishOptions?: { maxAttempts: number }; +} + +export type ChelActionParams = { + action: string; + server?: string; // TODO: implement! + contractID: string; + data: Object; + hooks?: { + prepublishContract?: (GIMessage) => void; + prepublish?: (GIMessage) => void; + postpublish?: (GIMessage) => void; + }; + publishOptions?: { maxAttempts: number }; +} + export { GIMessage } -export const ACTION_REGEX = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ +export const ACTION_REGEX: RegExp = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ // ACTION_REGEX.exec('gi.contracts/group/payment/process') // 0 => 'gi.contracts/group/payment/process' // 1 => 'gi.contracts/group/payment/' @@ -26,7 +50,7 @@ export const ACTION_REGEX = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ // 4 => 'group' // 5 => 'payment' -export default (sbp('sbp/selectors/register', { +export default sbp('sbp/selectors/register', { // https://www.wordnik.com/words/chelonia // https://gitlab.okturtles.org/okturtles/group-income/-/wikis/E2E-Protocol/Framework.md#alt-names 'chelonia/_init': function () { @@ -47,7 +71,7 @@ export default (sbp('sbp/selectors/register', { overrides: {}, // override default values per-contract manifests: {} // override! contract names => manifest hashes }, - whitelisted: (action) => !!this.whitelistedActions[action], + whitelisted: (action: string): boolean => !!this.whitelistedActions[action], reactiveSet: (obj, key, value) => { obj[key] = value; return value }, // example: set to Vue.set reactiveDel: (obj, key) => { delete obj[key] }, skipActionProcessing: false, @@ -73,8 +97,8 @@ export default (sbp('sbp/selectors/register', { } this.manifestToContract = {} this.whitelistedActions = {} - this.sideEffectStacks = {} // [contractID]: Array<*> - this.sideEffectStack = (contractID) => { + this.sideEffectStacks = {} // [contractID]: Array + this.sideEffectStack = (contractID: string): Array => { let stack = this.sideEffectStacks[contractID] if (!stack) { this.sideEffectStacks[contractID] = stack = [] @@ -85,7 +109,7 @@ export default (sbp('sbp/selectors/register', { 'chelonia/config': function () { return cloneDeep(this.config) }, - 'chelonia/configure': async function (config) { + 'chelonia/configure': async function (config: Object) { merge(this.config, config) // merge will strip the hooks off of config.hooks when merging from the root of the object // because they are functions and cloneDeep doesn't clone functions @@ -99,7 +123,7 @@ export default (sbp('sbp/selectors/register', { } }, // TODO: allow connecting to multiple servers at once - 'chelonia/connect': function () { + 'chelonia/connect': function (): Object { if (!this.config.connectionURL) throw new Error('config.connectionURL missing') if (!this.config.connectionOptions) throw new Error('config.connectionOptions missing') if (this.pubsub) { @@ -138,7 +162,7 @@ export default (sbp('sbp/selectors/register', { } return this.pubsub }, - 'chelonia/defineContract': function (contract) { + 'chelonia/defineContract': function (contract: Object) { if (!ACTION_REGEX.exec(contract.name)) throw new Error(`bad contract name: ${contract.name}`) if (!contract.metadata) contract.metadata = { validate () {}, create: () => ({}) } if (!contract.getters) contract.getters = {} @@ -152,7 +176,7 @@ export default (sbp('sbp/selectors/register', { [`${contract.manifest}/${contract.name}/getters`]: () => contract.getters, // 2 ways to cause sideEffects to happen: by defining a sideEffect function in the // contract, or by calling /pushSideEffect w/async SBP call. Can also do both. - [`${contract.manifest}/${contract.name}/pushSideEffect`]: (contractID, asyncSbpCall) => { + [`${contract.manifest}/${contract.name}/pushSideEffect`]: (contractID: string, asyncSbpCall: Array) => { // if this version of the contract is pushing a sideEffect to a function defined by the // contract itself, make sure that it calls the same version of the sideEffect const [sel] = asyncSbpCall @@ -172,7 +196,7 @@ export default (sbp('sbp/selectors/register', { // - whatever keys should be passed in as well // base it off of the design of encryptedAction() this.defContractSelectors.push(...sbp('sbp/selectors/register', { - [`${contract.manifest}/${action}/process`]: (message, state) => { + [`${contract.manifest}/${action}/process`]: (message: Object, state: Object) => { const { meta, data, contractID } = message // TODO: optimize so that you're creating a proxy object only when needed const gProxy = gettersProxy(state, contract.getters) @@ -181,7 +205,7 @@ export default (sbp('sbp/selectors/register', { contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) contract.actions[action].process(message, { state, ...gProxy }) }, - [`${contract.manifest}/${action}/sideEffect`]: async (message, state) => { + [`${contract.manifest}/${action}/sideEffect`]: async (message: Object, state: Object) => { const sideEffects = this.sideEffectStack(message.contractID) while (sideEffects.length > 0) { const sideEffect = sideEffects.shift() @@ -234,7 +258,7 @@ export default (sbp('sbp/selectors/register', { } }, // resolves when all pending actions for these contractID(s) finish - 'chelonia/contract/wait': function (contractIDs) { + 'chelonia/contract/wait': function (contractIDs?: string | string[]): Promise { const listOfIds = contractIDs ? (typeof contractIDs === 'string' ? [contractIDs] : contractIDs) : Object.keys(sbp(this.config.stateSelector).contracts) @@ -244,7 +268,7 @@ export default (sbp('sbp/selectors/register', { }, // 'chelonia/contract' - selectors related to injecting remote data and monitoring contracts // TODO: add an optional parameter to "retain" the contract (see #828) - 'chelonia/contract/sync': function (contractIDs) { + 'chelonia/contract/sync': function (contractIDs: string | string[]): Promise { const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs return Promise.all(listOfIds.map(contractID => { // enqueue this invocation in a serial queue to ensure @@ -262,7 +286,7 @@ export default (sbp('sbp/selectors/register', { }, // TODO: implement 'chelonia/contract/release' (see #828) // safer version of removeImmediately that waits to finish processing events for contractIDs - 'chelonia/contract/remove': function (contractIDs) { + 'chelonia/contract/remove': function (contractIDs: string | string[]): Promise { const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs return Promise.all(listOfIds.map(contractID => { return sbp('chelonia/queueInvocation', contractID, [ @@ -271,7 +295,7 @@ export default (sbp('sbp/selectors/register', { })) }, // Warning: avoid using this unless you know what you're doing. Prefer using /remove. - 'chelonia/contract/removeImmediately': function (contractID) { + 'chelonia/contract/removeImmediately': function (contractID: string) { const state = sbp(this.config.stateSelector) this.config.reactiveDel(state.contracts, contractID) this.config.reactiveDel(state, contractID) @@ -281,19 +305,19 @@ export default (sbp('sbp/selectors/register', { // TODO: r.body is a stream.Transform, should we use a callback to process // the events one-by-one instead of converting to giant json object? // however, note if we do that they would be processed in reverse... - 'chelonia/out/eventsSince': async function (contractID, since) { + 'chelonia/out/eventsSince': async function (contractID: string, since: string) { const events = await fetch(`${this.config.connectionURL}/eventsSince/${contractID}/${since}`) .then(handleFetchResult('json')) if (Array.isArray(events)) { return events.reverse().map(b64ToStr) } }, - 'chelonia/out/latestHash': function (contractID) { + 'chelonia/out/latestHash': function (contractID: string) { return fetch(`${this.config.connectionURL}/latestHash/${contractID}`, { cache: 'no-store' }).then(handleFetchResult('text')) }, - 'chelonia/out/eventsBefore': async function (before, limit) { + 'chelonia/out/eventsBefore': async function (before: string, limit: number) { if (limit <= 0) { console.error('[chelonia] invalid params error: "limit" needs to be positive integer') return @@ -305,20 +329,19 @@ export default (sbp('sbp/selectors/register', { return events.reverse().map(b64ToStr) } }, - 'chelonia/out/eventsBetween': async function (startHash, endHash, offset = 0) { + 'chelonia/out/eventsBetween': async function (startHash: string, endHash: string, offset: number = 0) { if (offset < 0) { console.error('[chelonia] invalid params error: "offset" needs to be positive integer or zero') return } const events = await fetch(`${this.config.connectionURL}/eventsBetween/${startHash}/${endHash}?offset=${offset}`) - .then(handleFetchResult('json')).catch(console.error) - console.log('events:', events) + .then(handleFetchResult('json')) if (Array.isArray(events)) { return events.reverse().map(b64ToStr) } }, - 'chelonia/latestContractState': async function (contractID) { + 'chelonia/latestContractState': async function (contractID: string) { const events = await sbp('chelonia/out/eventsSince', contractID, contractID) let state = {} // fast-path @@ -345,7 +368,7 @@ export default (sbp('sbp/selectors/register', { return state }, // 'chelonia/out' - selectors that send data out to the server - 'chelonia/out/registerContract': async function (params) { + 'chelonia/out/registerContract': async function (params: ChelRegParams) { const { contractName, hooks, publishOptions } = params const manifestHash = this.config.contracts.manifests[contractName] const contractInfo = this.manifestToContract[manifestHash] @@ -353,10 +376,10 @@ export default (sbp('sbp/selectors/register', { const contractMsg = GIMessage.createV1_0(null, null, [ GIMessage.OP_CONTRACT, - ({ + { type: contractName, keyJSON: 'TODO: add group public key here' - }) + } ], manifestHash ) @@ -373,10 +396,10 @@ export default (sbp('sbp/selectors/register', { }, // all of these functions will do both the creation of the GIMessage // and the sending of it via 'chelonia/private/out/publishEvent' - 'chelonia/out/actionEncrypted': function (params) { + 'chelonia/out/actionEncrypted': function (params: ChelActionParams): Promise { return outEncryptedOrUnencryptedAction.call(this, GIMessage.OP_ACTION_ENCRYPTED, params) }, - 'chelonia/out/actionUnencrypted': function (params) { + 'chelonia/out/actionUnencrypted': function (params: ChelActionParams): Promise { return outEncryptedOrUnencryptedAction.call(this, GIMessage.OP_ACTION_UNENCRYPTED, params) }, 'chelonia/out/keyAdd': async function () { @@ -394,9 +417,9 @@ export default (sbp('sbp/selectors/register', { 'chelonia/out/propDel': async function () { } -})) +}) -function contractNameFromAction (action) { +function contractNameFromAction (action: string): string { const regexResult = ACTION_REGEX.exec(action) const contractName = regexResult && regexResult[2] if (!contractName) throw new Error(`Poorly named action '${action}': missing contract name.`) @@ -404,8 +427,8 @@ function contractNameFromAction (action) { } async function outEncryptedOrUnencryptedAction ( - opType, - params + opType: 'ae' | 'au', + params: ChelActionParams ) { const { action, contractID, data, hooks, publishOptions } = params const contractName = contractNameFromAction(action) @@ -417,7 +440,7 @@ async function outEncryptedOrUnencryptedAction ( const gProxy = gettersProxy(state, contract.getters) contract.metadata.validate(meta, { state, ...gProxy, contractID }) contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) - const unencMessage = ({ action, data, meta }) + const unencMessage: GIOpActionUnencrypted = { action, data, meta } const message = GIMessage.createV1_0(contractID, previousHEAD, [ opType, @@ -438,7 +461,7 @@ async function outEncryptedOrUnencryptedAction ( // The only way to pass in the state is by creating a Proxy object that does // that for us. This allows us to maintain compatibility with Vue.js and integrate // the contract getters into the Vue-facing getters. -function gettersProxy (state, getters) { +function gettersProxy (state: Object, getters: Object) { const proxyGetters = new Proxy({}, { get (target, prop) { return getters[prop](state, proxyGetters) diff --git a/shared/domains/chelonia/db.js b/shared/domains/chelonia/db.ts similarity index 97% rename from shared/domains/chelonia/db.js rename to shared/domains/chelonia/db.ts index 8afd73e7fe..0c4d9d27b8 100644 --- a/shared/domains/chelonia/db.js +++ b/shared/domains/chelonia/db.ts @@ -1,10 +1,8 @@ -'use strict' - import sbp from '@sbp/sbp' import '@sbp/okturtles.data' import '@sbp/okturtles.eventqueue' -import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' -import { ChelErrorDBBadPreviousHEAD, ChelErrorDBConnection } from './errors.js' +import { GIMessage } from '~/shared/domains/chelonia/GIMessage.ts' +import { ChelErrorDBBadPreviousHEAD, ChelErrorDBConnection } from './errors.ts' const headSuffix = '-HEAD' diff --git a/shared/domains/chelonia/errors.js b/shared/domains/chelonia/errors.ts similarity index 98% rename from shared/domains/chelonia/errors.js rename to shared/domains/chelonia/errors.ts index 138c91a957..e19d112030 100644 --- a/shared/domains/chelonia/errors.js +++ b/shared/domains/chelonia/errors.ts @@ -1,5 +1,3 @@ -'use strict' - export class ChelErrorDBBadPreviousHEAD extends Error { // ugly boilerplate because JavaScript is stupid // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types diff --git a/shared/domains/chelonia/events.js b/shared/domains/chelonia/events.ts similarity index 100% rename from shared/domains/chelonia/events.js rename to shared/domains/chelonia/events.ts diff --git a/shared/domains/chelonia/internals.js b/shared/domains/chelonia/internals.ts similarity index 98% rename from shared/domains/chelonia/internals.js rename to shared/domains/chelonia/internals.ts index a5b09b70bc..d856afb893 100644 --- a/shared/domains/chelonia/internals.js +++ b/shared/domains/chelonia/internals.ts @@ -1,18 +1,16 @@ -'use strict' - import sbp, { domainFromSelector } from '@sbp/sbp' -import './db.js' -import { GIMessage } from './GIMessage.js' +import './db.ts' +import { GIMessage } from './GIMessage.ts' import { randomIntFromRange, delay, cloneDeep, debounce, pick } from '~/frontend/model/contracts/shared/giLodash.js' -import { ChelErrorUnexpected, ChelErrorUnrecoverable } from './errors.js' -import { CONTRACT_IS_SYNCING, CONTRACTS_MODIFIED, EVENT_HANDLED } from './events.js' +import { ChelErrorUnexpected, ChelErrorUnrecoverable } from './errors.ts' +import { CONTRACT_IS_SYNCING, CONTRACTS_MODIFIED, EVENT_HANDLED } from './events.ts' import { handleFetchResult } from '~/frontend/controller/utils/misc.js' -import { blake32Hash } from '~/shared/functions.js' +import { blake32Hash } from '~/shared/functions.ts' // import 'ses' // export const FERAL_FUNCTION = Function -export default (sbp('sbp/selectors/register', { +export default sbp('sbp/selectors/register', { // DO NOT CALL ANY OF THESE YOURSELF! 'chelonia/private/state': function () { return this.state @@ -64,7 +62,7 @@ export default (sbp('sbp/selectors/register', { has (o, p) { /* console.log('has', p); */ return true } })) { (function () { - 'use strict' + 'use strict'; ${source} })() } @@ -82,12 +80,12 @@ export default (sbp('sbp/selectors/register', { console, Object, Error, + Function, // TODO: remove this TypeError, Math, Symbol, Date, Array, - // $FlowFixMe BigInt, Boolean, String, @@ -327,7 +325,7 @@ export default (sbp('sbp/selectors/register', { throw e } } -})) +}) const eventsToReinjest = [] const reprocessDebounced = debounce((contractID) => sbp('chelonia/contract/sync', contractID), 1000) diff --git a/shared/functions.js b/shared/functions.ts similarity index 100% rename from shared/functions.js rename to shared/functions.ts diff --git a/shared/pubsub.test.js b/shared/pubsub.test.js deleted file mode 100644 index 2cf442b6a0..0000000000 --- a/shared/pubsub.test.js +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -import { createClient } from './pubsub.js' - -const should = require('should') // eslint-disable-line - -const client = createClient('ws://localhost:8080', { - manual: true, - reconnectOnDisconnection: false, - reconnectOnOnline: false, - reconnectOnTimeout: false -}) -const { - maxReconnectionDelay, - minReconnectionDelay -} = client.options - -const createRandomDelays = (number) => { - return [...new Array(number)].map((_, i) => { - client.failedConnectionAttempts = i - return client.getNextRandomDelay() - }) -} -const delays1 = createRandomDelays(10) -const delays2 = createRandomDelays(10) - -describe('Test getNextRandomDelay()', function () { - it('every delay should be longer than the previous one', function () { - // In other words, the delays should be sorted in ascending numerical order. - should(delays1).deepEqual([...delays1].sort((a, b) => a - b)) - should(delays2).deepEqual([...delays2].sort((a, b) => a - b)) - }) - - it('no delay should be shorter than the minimal reconnection delay', function () { - delays1.forEach((delay) => { - should(delay).be.greaterThanOrEqual(minReconnectionDelay) - }) - delays2.forEach((delay) => { - should(delay).be.greaterThanOrEqual(minReconnectionDelay) - }) - }) - - it('no delay should be longer than the maximal reconnection delay', function () { - delays1.forEach((delay) => { - should(delay).be.lessThanOrEqual(maxReconnectionDelay) - }) - delays2.forEach((delay) => { - should(delay).be.lessThanOrEqual(maxReconnectionDelay) - }) - }) -}) diff --git a/shared/pubsub.test.ts b/shared/pubsub.test.ts new file mode 100644 index 0000000000..9eecfd0792 --- /dev/null +++ b/shared/pubsub.test.ts @@ -0,0 +1,64 @@ +import { + assert, + assertEquals, + assertMatch, + assertNotMatch +} from "https://deno.land/std@0.153.0/testing/asserts.ts" + +import '~/scripts/process-shim.ts' + +import { createClient } from './pubsub.ts' + +const client = createClient('ws://localhost:8080', { + manual: true, + reconnectOnDisconnection: false, + reconnectOnOnline: false, + reconnectOnTimeout: false +}) +const { + maxReconnectionDelay, + minReconnectionDelay +} = client.options + +const createRandomDelays = (number) => { + return [...new Array(number)].map((_, i) => { + client.failedConnectionAttempts = i + return client.getNextRandomDelay() + }) +} +const delays1 = createRandomDelays(10) +const delays2 = createRandomDelays(10) + + +Deno.test({ + name: 'Test getNextRandomDelay()', + fn: async function (tests) { + + await tests.step('every delay should be longer than the previous one', async function () { + // In other words, the delays should be sorted in ascending numerical order. + assertEquals(delays1, [...delays1].sort((a, b) => a - b)) + assertEquals(delays2, [...delays2].sort((a, b) => a - b)) + }) + + await tests.step('no delay should be shorter than the minimal reconnection delay', async function () { + delays1.forEach((delay) => { + assert(delay >= minReconnectionDelay) + }) + delays2.forEach((delay) => { + assert(delay >= minReconnectionDelay) + }) + }) + + await tests.step('no delay should be longer than the maximal reconnection delay', async function () { + delays1.forEach((delay) => { + assert(delay <= maxReconnectionDelay) + }) + delays2.forEach((delay) => { + assert(delay <= maxReconnectionDelay) + }) + }) + }, + sanitizeResources: false, + sanitizeOps: false, +}) + diff --git a/shared/pubsub.js b/shared/pubsub.ts similarity index 100% rename from shared/pubsub.js rename to shared/pubsub.ts diff --git a/shared/string.js b/shared/string.ts similarity index 59% rename from shared/string.js rename to shared/string.ts index 032649b229..2be0de3b9c 100644 --- a/shared/string.js +++ b/shared/string.ts @@ -1,6 +1,6 @@ 'use strict' -export function dasherize (s) { +export function dasherize (s: string): string { return s .trim() .replace(/[_\s]+/g, '-') @@ -9,29 +9,29 @@ export function dasherize (s) { .toLowerCase() } -export function capitalize (s) { +export function capitalize (s: string): string { return s.substr(0, 1).toUpperCase() + s.substring(1).toLowerCase() } -export function camelize (s) { +export function camelize (s: string): string { return s.trim().replace(/(-|_|\s)+(.)?/g, (mathc, sep, c) => { return c ? c.toUpperCase() : '' }) } -export function startsWith (s, what) { +export function startsWith (s: string, what: string): boolean { return s.indexOf(what) === 0 } -export function endsWith (s, what) { +export function endsWith (s: string, what: string): boolean { const len = s.length - what.length return len >= 0 && s.indexOf(what, len) === len } -export function chompLeft (s, what) { +export function chompLeft (s: string, what: string): string { return s.indexOf(what) === 0 ? s.slice(what.length) : s } -export function chompRight (s, what) { +export function chompRight (s: string, what: string): string { return endsWith(s, what) ? s.slice(0, s.length - what.length) : s } diff --git a/shared/types.js b/shared/types.ts similarity index 100% rename from shared/types.js rename to shared/types.ts diff --git a/test/avatar-caching.test.js b/test/avatar-caching.test.js deleted file mode 100644 index 6b70ae4694..0000000000 --- a/test/avatar-caching.test.js +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-env mocha */ - -const assert = require('assert') -const { copyFile } = require('fs/promises') - -describe('avatar file serving', function () { - const apiURL = process.env.API_URL - const hash = '21XWnNX5exusmJoJNWNNqjhWPqxGURryWbkUhYVsGT5NFtSGKs' - - before('manually upload a test avatar to the file database', async () => { - await copyFile(`./test/data/${hash}`, `./data/${hash}`) - }) - - it('Should serve our test avatar with correct headers', async function () { - const { headers } = await fetch(`${apiURL}/file/${hash}`) - - assert.match(headers.get('cache-control'), /immutable/) - assert.doesNotMatch(headers.get('cache-control'), /no-cache/) - assert.equal(headers.get('content-length'), '405') - assert.equal(headers.get('content-type'), 'application/octet-stream') - assert.equal(headers.get('etag'), `"${hash}"`) - assert(headers.has('last-modified')) - assert.equal(headers.get('x-frame-options'), 'deny') - }) -}) diff --git a/test/avatar-caching.test.ts b/test/avatar-caching.test.ts new file mode 100644 index 0000000000..77a4e4be8e --- /dev/null +++ b/test/avatar-caching.test.ts @@ -0,0 +1,37 @@ +import { + assert, + assertEquals, + assertMatch, + assertNotMatch +} from "https://deno.land/std@0.153.0/testing/asserts.ts" + +import { bold, red, yellow } from 'fmt/colors.ts' +import * as pathlib from 'path' + +import '~/scripts/process-shim.ts' + +Deno.test({ + name: 'Avatar file serving', + fn: async function (tests) { + + const apiURL = process.env.API_URL ?? 'http://localhost:8000' + const hash = '21XWnNX5exusmJoJNWNNqjhWPqxGURryWbkUhYVsGT5NFtSGKs' + + // Manually upload a test avatar to the file database. + await Deno.copyFile(`./test/data/${hash}`, `./data/${hash}`) + + await tests.step('Should serve our test avatar with correct headers', async function () { + const { headers } = await fetch(`${apiURL}/file/${hash}`) + + assertMatch(headers.get('cache-control'), /immutable/) + assertNotMatch(headers.get('cache-control'), /no-cache/) + assertEquals(headers.get('content-length'), '405') + assertEquals(headers.get('content-type'), 'application/octet-stream') + assertEquals(headers.get('etag'), `"${hash}"`) + assert(headers.has('last-modified')) + assertEquals(headers.get('x-frame-options'), 'deny') + }) + }, + sanitizeResources: false, + sanitizeOps: false, +}) diff --git a/test/backend.test.js b/test/backend.test.js deleted file mode 100644 index 60e9ffd2aa..0000000000 --- a/test/backend.test.js +++ /dev/null @@ -1,387 +0,0 @@ -/* eslint-env mocha */ - -import sbp from '@sbp/sbp' -import '@sbp/okturtles.events' -import '@sbp/okturtles.eventqueue' -import '~/shared/domains/chelonia/chelonia.js' -import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' -import { handleFetchResult } from '~/frontend/controller/utils/misc.js' -import { blake32Hash } from '~/shared/functions.js' -import * as Common from '@common/common.js' -import proposals from '~/frontend/model/contracts/shared/voting/proposals.js' -import { PAYMENT_PENDING, PAYMENT_TYPE_MANUAL } from '~/frontend/model/contracts/shared/payments/index.js' -import { INVITE_INITIAL_CREATOR, INVITE_EXPIRES_IN_DAYS, MAIL_TYPE_MESSAGE, PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC } from '~/frontend/model/contracts/shared/constants.js' -import { createInvite } from '~/frontend/model/contracts/shared/functions.js' -import '~/frontend/controller/namespace.js' -import chalk from 'chalk' -import { THEME_LIGHT } from '~/frontend/utils/themes.js' -import manifests from '~/frontend/model/contracts/manifests.json' - -// Necessary since we are going to use a WebSocket pubsub client in the backend. -global.WebSocket = require('ws') -const should = require('should') // eslint-disable-line - -// Remove this when dropping support for Node versions lower than v18. -const Blob = require('buffer').Blob -const fs = require('fs') -const path = require('path') -// const { PassThrough, Readable } = require('stream') - -chalk.level = 2 // for some reason it's not detecting that terminal supports colors -const { bold } = chalk - -// var unsignedMsg = sign(personas[0], 'futz') - -// TODO: replay attacks? (need server-provided challenge for `msg`?) -// nah, this should be taken care of by TLS. However, for message -// passing we should be using a forward-secure protocol. See -// MessageRelay in interface.js. - -// TODO: the request for members of a group should be made with a group -// key or a group signature. There should not be a mapping of a -// member's key to all the groups that they're in (that's unweildy -// and compromises privacy). - -const vuexState = { - currentGroupId: null, - currentChatRoomIDs: {}, - contracts: {}, // contractIDs => { type:string, HEAD:string } (for contracts we've successfully subscribed to) - pending: [], // contractIDs we've just published but haven't received back yet - loggedIn: false, // false | { username: string, identityContractID: string } - theme: THEME_LIGHT, - fontSize: 1, - increasedContrast: false, - reducedMotion: false, - appLogsFilter: ['error', 'info', 'warn'] -} - -// this is to ensure compatibility between frontend and test/backend.test.js -sbp('okTurtles.data/set', 'API_URL', process.env.API_URL) - -sbp('sbp/selectors/register', { - // for handling the loggedIn metadata() in Contracts.js - 'state/vuex/state': () => { - return vuexState - } -}) - -sbp('sbp/selectors/register', { - 'backend.tests/postEntry': async function (entry) { - console.log(bold.yellow('sending entry with hash:'), entry.hash()) - const res = await sbp('chelonia/private/out/publishEvent', entry) - should(res).equal(entry.hash()) - return res - } -}) - -// uncomment this to help with debugging: -// sbp('sbp/filters/global/add', (domain, selector, data) => { -// console.log(`[sbp] ${selector}:`, data) -// }) - -describe('Full walkthrough', function () { - const users = {} - const groups = {} - - it('Should configure chelonia', async function () { - await sbp('chelonia/configure', { - connectionURL: process.env.API_URL, - stateSelector: 'state/vuex/state', - skipSideEffects: true, - connectionOptions: { - reconnectOnDisconnection: false, - reconnectOnOnline: false, - reconnectOnTimeout: false, - timeout: 3000 - }, - contracts: { - ...manifests, - defaults: { - modules: { '@common/common.js': Common }, - allowedSelectors: [ - 'state/vuex/state', 'state/vuex/commit', 'state/vuex/getters', - 'chelonia/contract/sync', 'chelonia/contract/remove', 'controller/router', - 'chelonia/queueInvocation', 'gi.actions/identity/updateLoginStateUponLogin', - 'gi.actions/chatroom/leave', 'gi.notifications/emit' - ], - allowedDomains: ['okTurtles.data', 'okTurtles.events', 'okTurtles.eventQueue'], - preferSlim: true - } - } - }) - }) - - function login (user) { - // we set this so that the metadata on subsequent messages is properly filled in - // currently group and mailbox contracts use this to determine message sender - vuexState.loggedIn = { - username: user.decryptedValue().data.attributes.username, - identityContractID: user.contractID() - } - } - - async function createIdentity (username, email, testFn) { - // append random id to username to prevent conflict across runs - // when GI_PERSIST environment variable is defined - username = `${username}-${Math.floor(Math.random() * 1000)}` - const msg = await sbp('chelonia/out/registerContract', { - contractName: 'gi.contracts/identity', - data: { - // authorizations: [Events.CanModifyAuths.dummyAuth(name)], - attributes: { username, email } - }, - hooks: { - prepublish: (message) => { message.decryptedValue(JSON.parse) }, - postpublish: (message) => { testFn && testFn(message) } - } - }) - return msg - } - function createGroup (name: string, hooks: Object = {}): Promise { - const initialInvite = createInvite({ - quantity: 60, - creator: INVITE_INITIAL_CREATOR, - expires: INVITE_EXPIRES_IN_DAYS.ON_BOARDING - }) - return sbp('chelonia/out/registerContract', { - contractName: 'gi.contracts/group', - data: { - invites: { - [initialInvite.inviteSecret]: initialInvite - }, - settings: { - // authorizations: [Events.CanModifyAuths.dummyAuth(name)], - groupName: name, - groupPicture: '', - sharedValues: 'our values', - mincomeAmount: 1000, - mincomeCurrency: 'USD', - distributionDate: new Date().toISOString(), - minimizeDistribution: true, - proposals: { - [PROPOSAL_GROUP_SETTING_CHANGE]: proposals[PROPOSAL_GROUP_SETTING_CHANGE].defaults, - [PROPOSAL_INVITE_MEMBER]: proposals[PROPOSAL_INVITE_MEMBER].defaults, - [PROPOSAL_REMOVE_MEMBER]: proposals[PROPOSAL_REMOVE_MEMBER].defaults, - [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposals[PROPOSAL_PROPOSAL_SETTING_CHANGE].defaults, - [PROPOSAL_GENERIC]: proposals[PROPOSAL_GENERIC].defaults - } - } - }, - hooks - }) - } - function createPaymentTo (to, amount, contractID, currency = 'USD'): Promise { - return sbp('chelonia/out/actionEncrypted', { - action: 'gi.contracts/group/payment', - data: { - toUser: to.decryptedValue().data.attributes.username, - amount: amount, - currency: currency, - txid: String(parseInt(Math.random() * 10000000)), - status: PAYMENT_PENDING, - paymentType: PAYMENT_TYPE_MANUAL - }, - contractID - }) - } - - async function createMailboxFor (user) { - const mailbox = await sbp('chelonia/out/registerContract', { - contractName: 'gi.contracts/mailbox', - data: {} - }) - await sbp('chelonia/out/actionEncrypted', { - action: 'gi.contracts/identity/setAttributes', - data: { mailbox: mailbox.contractID() }, - contractID: user.contractID() - }) - user.mailbox = mailbox - await sbp('chelonia/contract/sync', mailbox.contractID()) - return mailbox - } - - describe('Identity tests', function () { - it('Should create identity contracts for Alice and Bob', async function () { - users.bob = await createIdentity('bob', 'bob@okturtles.com') - users.alice = await createIdentity('alice', 'alice@okturtles.org') - // verify attribute creation and state initialization - users.bob.decryptedValue().data.attributes.username.should.match(/^bob/) - users.bob.decryptedValue().data.attributes.email.should.equal('bob@okturtles.com') - }) - - it('Should register Alice and Bob in the namespace', async function () { - const { alice, bob } = users - let res = await sbp('namespace/register', alice.decryptedValue().data.attributes.username, alice.contractID()) - // NOTE: don't rely on the return values for 'namespace/register' - // too much... in the future we might remove these checks - res.value.should.equal(alice.contractID()) - res = await sbp('namespace/register', bob.decryptedValue().data.attributes.username, bob.contractID()) - res.value.should.equal(bob.contractID()) - alice.socket = 'hello' - should(alice.socket).equal('hello') - }) - - it('Should verify namespace lookups work', async function () { - const { alice } = users - const res = await sbp('namespace/lookup', alice.decryptedValue().data.attributes.username) - res.should.equal(alice.contractID()) - const contractID = await sbp('namespace/lookup', 'susan') - should(contractID).equal(null) - }) - - it('Should open socket for Alice', async function () { - users.alice.socket = await sbp('chelonia/connect') - }) - - it('Should create mailboxes for Alice and Bob and subscribe', async function () { - // Object.values(users).forEach(async user => await createMailboxFor(user)) - await createMailboxFor(users.alice) - await createMailboxFor(users.bob) - }) - }) - - describe('Group tests', function () { - it('Should create a group & subscribe Alice', async function () { - // set user Alice as being logged in so that metadata on messages is properly set - login(users.alice) - groups.group1 = await createGroup('group1') - await sbp('chelonia/contract/sync', groups.group1.contractID()) - }) - - // NOTE: The frontend needs to use the `fetch` API instead of superagent because - // superagent doesn't support streaming, whereas fetch does. - // TODO: We should also remove superagent as a dependency since `fetch` does - // everything we need. Use fetch from now on. - it('Should get mailbox info for Bob', async function () { - // 1. look up bob's username to get his identity contract - const { bob } = users - const bobsName = bob.decryptedValue().data.attributes.username - const bobsContractId = await sbp('namespace/lookup', bobsName) - should(bobsContractId).equal(bob.contractID()) - // 2. fetch all events for his identity contract to get latest state for it - const state = await sbp('chelonia/latestContractState', bobsContractId) - console.log(bold.red('FINAL STATE:'), state) - // 3. get bob's mailbox contractID from his identity contract attributes - should(state.attributes.mailbox).equal(bob.mailbox.contractID()) - // 4. fetch the latest hash for bob's mailbox. - // we don't need latest state for it just latest hash - const res = await sbp('chelonia/out/latestHash', state.attributes.mailbox) - should(res).equal(bob.mailbox.hash()) - }) - - it("Should invite Bob to Alice's group", function (done) { - const mailbox = users.bob.mailbox - sbp('chelonia/out/actionEncrypted', { - action: 'gi.contracts/mailbox/postMessage', - data: { - from: users.bob.decryptedValue().data.attributes.username, - messageType: MAIL_TYPE_MESSAGE, - message: groups.group1.contractID() - }, - contractID: mailbox.contractID(), - hooks: { - prepublish (invite: GIMessage) { - sbp('okTurtles.events/once', invite.hash(), (contractID: string, entry: GIMessage) => { - console.debug('Bob successfully got invite!') - should(entry.decryptedValue().data.message).equal(groups.group1.contractID()) - done() - }) - } - } - }) - }) - - it('Should post an event', function () { - return createPaymentTo(users.bob, 100, groups.group1.contractID()) - }) - - it('Should sync group and verify payments in state', async function () { - await sbp('chelonia/contract/sync', groups.group1.contractID()) - should(Object.keys(vuexState[groups.group1.contractID()].payments).length).equal(1) - }) - - it('Should fail with wrong contractID', async function () { - try { - await createPaymentTo(users.bob, 100, '') - return Promise.reject(new Error("shouldn't get here!")) - } catch (e) { - return Promise.resolve() - } - }) - - // TODO: these events, as well as all messages sent over the sockets - // should all be authenticated and identified by the user's - // identity contract - }) - - describe('File upload', function () { - it('Should upload "avatar-default.png" as "multipart/form-data"', async function () { - const form = new FormData() - const filepath = './frontend/assets/images/user-avatar-default.png' - // const context = blake2bInit(32, null) - // const stream = fs.createReadStream(filepath) - // // the problem here is that we need to get the hash of the file - // // but doing so consumes the stream, invalidating it and making it - // // so that we can't simply do `form.append('data', stream)` - // // I tried creating a secondary piped stream and sending that instead, - // // however that didn't work. - // // const pass = new PassThrough() // couldn't get this or Readable to work no matter how I tried - // // So instead we save the raw buffer and send that, using a hack - // // to work around a weird bug in hapi or form-data where we have to - // // specify the filename or otherwise the backend treats the data as a string, - // // resulting in the wrong hash for some reason. By specifying `filename` the backend - // // treats it as a Buffer, and we get the correct file hash. - // // We could of course simply read the file twice, but that seems even more hackish. - // var buffer - // const hash = await new Promise((resolve, reject) => { - // stream.on('error', e => reject(e)) - // stream.on('data', chunk => { - // buffer = buffer ? Buffer.concat([buffer, chunk]) : chunk - // blake2bUpdate(context, chunk) - // }) - // stream.on('end', () => { - // const uint8array = blake2bFinal(context) - // resolve(multihash.toB58String(multihash.encode(Buffer.from(uint8array.buffer), 'blake2b-32', 32))) - // }) - // }) - // since we're just saving the buffer now, we might as well use the simpler readFileSync API - const buffer = fs.readFileSync(filepath) - const hash = blake32Hash(buffer) - console.log(`hash for ${path.basename(filepath)}: ${hash}`) - form.append('hash', hash) - const blob = new Blob([buffer]) - form.append('data', blob, path.basename(filepath)) - await fetch(`${process.env.API_URL}/file`, { method: 'POST', body: form }) - .then(handleFetchResult('text')) - .then(r => should(r).equal(`/file/${hash}`)) - }) - }) - - describe('Cleanup', function () { - it('Should destroy all opened sockets', function () { - // The code below was originally Object.values(...) but changed to .keys() - // due to a similar flow issue to https://github.com/facebook/flow/issues/2221 - Object.keys(users).forEach((userKey) => { - users[userKey].socket && users[userKey].socket.destroy() - }) - }) - }) -}) - -// Potentially useful for dealing with fetch API: -// function streamToPromise (stream, dataHandler) { -// return new Promise((resolve, reject) => { -// stream.on('data', (...args) => { -// try { dataHandler(...args) } catch (err) { reject(err) } -// }) -// stream.on('end', resolve) -// stream.on('error', reject) -// }) -// } -// see: https://github.com/bitinn/node-fetch/blob/master/test/test.js -// This used to be in the 'Should get mailbox info for Bob' test, before the -// server manually created a JSON array out of the objects being streamed. -// await streamToPromise(res.body, chunk => { -// console.log(bold.red('CHUNK:'), chunk.toString()) -// events.push(JSON.parse(chunk.toString())) -// }) diff --git a/test/backend.test.ts b/test/backend.test.ts new file mode 100644 index 0000000000..bff3e91737 --- /dev/null +++ b/test/backend.test.ts @@ -0,0 +1,436 @@ +import { assertEquals, assertMatch } from "https://deno.land/std@0.153.0/testing/asserts.ts" + +import { bold, red, yellow } from 'fmt/colors.ts' +import * as pathlib from 'path' + +import '~/scripts/process-shim.ts' + +import sbp from '@sbp/sbp' +import '@sbp/okturtles.events' +import '@sbp/okturtles.eventqueue' +import '~/shared/domains/chelonia/chelonia.ts' +import { GIMessage } from '~/shared/domains/chelonia/GIMessage.ts' +import { handleFetchResult } from '~/frontend/controller/utils/misc.js' +import { blake32Hash } from '~/shared/functions.ts' +import * as Common from './common/common.js' +import proposals from './contracts/shared/voting/proposals.js' +import { PAYMENT_PENDING, PAYMENT_TYPE_MANUAL } from './contracts/shared/payments/index.js' +import { INVITE_INITIAL_CREATOR, INVITE_EXPIRES_IN_DAYS, MAIL_TYPE_MESSAGE, PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC } from './contracts/shared/constants.js' +import { createInvite } from './contracts/shared/functions.js' +import '~/frontend/controller/namespace.js' +import { THEME_LIGHT } from '~/frontend/utils/themes.js' +import manifests from '~/frontend/model/contracts/manifests.json' assert { type: "json" } + +// var unsignedMsg = sign(personas[0], 'futz') + +// TODO: replay attacks? (need server-provided challenge for `msg`?) +// nah, this should be taken care of by TLS. However, for message +// passing we should be using a forward-secure protocol. See +// MessageRelay in interface.js. + +// TODO: the request for members of a group should be made with a group +// key or a group signature. There should not be a mapping of a +// member's key to all the groups that they're in (that's unweildy +// and compromises privacy). + +/** + * Creates a modified copy of the given `process.env` object, according to its `PORT_SHIFT` variable. + * + * The `API_PORT` and `API_URL` variables will be updated. + * TODO: make the protocol (http vs https) variable based on environment var. + * @param {Object} env + * @returns {Object} + */ +const applyPortShift = (env) => { + // TODO: implement automatic port selection when `PORT_SHIFT` is 'auto'. + const API_PORT = 8000 + Number.parseInt(env.PORT_SHIFT || '0') + const API_URL = 'http://127.0.0.1:' + API_PORT + + if (Number.isNaN(API_PORT) || API_PORT < 8000 || API_PORT > 65535) { + throw new RangeError(`Invalid API_PORT value: ${API_PORT}.`) + } + return { ...env, API_PORT, API_URL } +} + +Object.assign(process.env, applyPortShift(process.env)) + +const { default: { version }} = await import('~/package.json', { + assert: { type: "json" }, +}) + +Deno.env.set('GI_VERSION', `${version}@${new Date().toISOString()}`) + +const API_PORT = Deno.env.get('API_PORT') +const API_URL = Deno.env.get('API_URL') +const CI = Deno.env.get('CI') +const GI_VERSION = Deno.env.get('GI_VERSION') +const NODE_ENV = Deno.env.get('NODE_ENV') ?? 'development' + +console.info('GI_VERSION:', GI_VERSION) +console.info('NODE_ENV:', NODE_ENV) + +const vuexState = { + currentGroupId: null, + currentChatRoomIDs: {}, + contracts: {}, // contractIDs => { type:string, HEAD:string } (for contracts we've successfully subscribed to) + pending: [], // contractIDs we've just published but haven't received back yet + loggedIn: false, // false | { username: string, identityContractID: string } + theme: THEME_LIGHT, + fontSize: 1, + increasedContrast: false, + reducedMotion: false, + appLogsFilter: ['error', 'info', 'warn'] +} + +// this is to ensure compatibility between frontend and test/backend.test.js +sbp('okTurtles.data/set', 'API_URL', process.env.API_URL) + +sbp('sbp/selectors/register', { + // for handling the loggedIn metadata() in Contracts.js + 'state/vuex/state': () => { + return vuexState + } +}) + +sbp('sbp/selectors/register', { + 'backend.tests/postEntry': async function (entry) { + console.log(bold(yellow('sending entry with hash:'), entry.hash())) + const res = await sbp('chelonia/private/out/publishEvent', entry) + assertEquals(res, entry.hash()) + return res + } +}) + +// uncomment this to help with debugging: +// sbp('sbp/filters/global/add', (domain, selector, data) => { +// console.log(`[sbp] ${selector}:`, data) +// }) + +Deno.test({ + name: 'Full walkthrough', + fn: async function (tests) { + const users = {} + const groups = {} + + // Wait for the server to be ready. + let t0 = Date.now() + let timeout = 3000 + await new Promise((resolve, reject) => { + (function ping () { + fetch(process.env.API_URL).then(resolve).catch(() => { + if (Date.now() > t0 + timeout) { + reject(new Error('Test setup timed out.')) + } else { + setTimeout(ping, 100) + } + }) + })() + }) + + await tests.step('Should configure chelonia', async function () { + await sbp('chelonia/configure', { + connectionURL: process.env.API_URL, + stateSelector: 'state/vuex/state', + skipSideEffects: true, + connectionOptions: { + reconnectOnDisconnection: false, + reconnectOnOnline: false, + reconnectOnTimeout: false, + timeout: 3000 + }, + contracts: { + ...manifests, + defaults: { + modules: { '@common/common.js': Common }, + allowedSelectors: [ + 'state/vuex/state', 'state/vuex/commit', 'state/vuex/getters', + 'chelonia/contract/sync', 'chelonia/contract/remove', 'controller/router', + 'chelonia/queueInvocation', 'gi.actions/identity/updateLoginStateUponLogin', + 'gi.actions/chatroom/leave', 'gi.notifications/emit' + ], + allowedDomains: ['okTurtles.data', 'okTurtles.events', 'okTurtles.eventQueue'], + preferSlim: true + } + } + }) + }) + + function login (user) { + // we set this so that the metadata on subsequent messages is properly filled in + // currently group and mailbox contracts use this to determine message sender + vuexState.loggedIn = { + username: user.decryptedValue().data.attributes.username, + identityContractID: user.contractID() + } + } + + async function createIdentity (username, email, testFn) { + // append random id to username to prevent conflict across runs + // when GI_PERSIST environment variable is defined + username = `${username}-${Math.floor(Math.random() * 1000)}` + const msg = await sbp('chelonia/out/registerContract', { + contractName: 'gi.contracts/identity', + data: { + // authorizations: [Events.CanModifyAuths.dummyAuth(name)], + attributes: { username, email } + }, + hooks: { + prepublish: (message) => { message.decryptedValue(JSON.parse) }, + postpublish: (message) => { testFn && testFn(message) } + } + }) + return msg + } + function createGroup (name: string, hooks: Object = {}): Promise { + const initialInvite = createInvite({ + quantity: 60, + creator: INVITE_INITIAL_CREATOR, + expires: INVITE_EXPIRES_IN_DAYS.ON_BOARDING + }) + return sbp('chelonia/out/registerContract', { + contractName: 'gi.contracts/group', + data: { + invites: { + [initialInvite.inviteSecret]: initialInvite + }, + settings: { + // authorizations: [Events.CanModifyAuths.dummyAuth(name)], + groupName: name, + groupPicture: '', + sharedValues: 'our values', + mincomeAmount: 1000, + mincomeCurrency: 'USD', + distributionDate: new Date().toISOString(), + minimizeDistribution: true, + proposals: { + [PROPOSAL_GROUP_SETTING_CHANGE]: proposals[PROPOSAL_GROUP_SETTING_CHANGE].defaults, + [PROPOSAL_INVITE_MEMBER]: proposals[PROPOSAL_INVITE_MEMBER].defaults, + [PROPOSAL_REMOVE_MEMBER]: proposals[PROPOSAL_REMOVE_MEMBER].defaults, + [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposals[PROPOSAL_PROPOSAL_SETTING_CHANGE].defaults, + [PROPOSAL_GENERIC]: proposals[PROPOSAL_GENERIC].defaults + } + } + }, + hooks + }) + } + function createPaymentTo (to, amount, contractID, currency = 'USD'): Promise { + return sbp('chelonia/out/actionEncrypted', { + action: 'gi.contracts/group/payment', + data: { + toUser: to.decryptedValue().data.attributes.username, + amount: amount, + currency: currency, + txid: String(parseInt(Math.random() * 10000000)), + status: PAYMENT_PENDING, + paymentType: PAYMENT_TYPE_MANUAL + }, + contractID + }) + } + + async function createMailboxFor (user) { + const mailbox = await sbp('chelonia/out/registerContract', { + contractName: 'gi.contracts/mailbox', + data: {} + }) + await sbp('chelonia/out/actionEncrypted', { + action: 'gi.contracts/identity/setAttributes', + data: { mailbox: mailbox.contractID() }, + contractID: user.contractID() + }) + user.mailbox = mailbox + await sbp('chelonia/contract/sync', mailbox.contractID()) + return mailbox + } + + await tests.step('Identity tests', async function (t) { + await t.step('Should create identity contracts for Alice and Bob', async function () { + users.bob = await createIdentity('bob', 'bob@okturtles.com') + users.alice = await createIdentity('alice', 'alice@okturtles.org') + // verify attribute creation and state initialization + assertMatch(users.bob.decryptedValue().data.attributes.username, /^bob/) + assertEquals(users.bob.decryptedValue().data.attributes.email, 'bob@okturtles.com') + }) + + await t.step('Should register Alice and Bob in the namespace', async function () { + const { alice, bob } = users + let res = await sbp('namespace/register', alice.decryptedValue().data.attributes.username, alice.contractID()) + // NOTE: don't rely on the return values for 'namespace/register' + // too much... in the future we might remove these checks + assertEquals(res.value, alice.contractID()) + res = await sbp('namespace/register', bob.decryptedValue().data.attributes.username, bob.contractID()) + assertEquals(res.value, bob.contractID()) + alice.socket = 'hello' + assertEquals(alice.socket, 'hello') + }) + + await t.step('Should verify namespace lookups work', async function () { + const { alice } = users + const username = alice.decryptedValue().data.attributes.username + const res = await sbp('namespace/lookup', username) + assertEquals(res, alice.contractID()) + const contractID = await sbp('namespace/lookup', 'susan') + assertEquals(contractID, null) + }) + + await t.step('Should open socket for Alice', async function () { + users.alice.socket = await sbp('chelonia/connect') + }) + + await t.step('Should create mailboxes for Alice and Bob and subscribe', async function () { + await createMailboxFor(users.alice) + await createMailboxFor(users.bob) + }) + }) + + await tests.step('Group tests', async function (t) { + await t.step('Should create a group & subscribe Alice', async function () { + // Set user Alice as being logged in so that metadata on messages is properly set. + login(users.alice) + groups.group1 = await createGroup('group1') + await sbp('chelonia/contract/sync', groups.group1.contractID()) + }) + + // NOTE: The frontend needs to use the `fetch` API instead of superagent because + // superagent doesn't support streaming, whereas fetch does. + // TODO: We should also remove superagent as a dependency since `fetch` does + // everything we need. Use fetch from now on. + await t.step('Should get mailbox info for Bob', async function () { + // 1. look up bob's username to get his identity contract + const { bob } = users + const bobsName = bob.decryptedValue().data.attributes.username + const bobsContractId = await sbp('namespace/lookup', bobsName) + assertEquals(bobsContractId, bob.contractID()) + // 2. fetch all events for his identity contract to get latest state for it + const state = await sbp('chelonia/latestContractState', bobsContractId) + console.log(bold(red('FINAL STATE:'), state)) + // 3. get bob's mailbox contractID from his identity contract attributes + assertEquals(state.attributes.mailbox, bob.mailbox.contractID()) + // 4. fetch the latest hash for bob's mailbox. + // we don't need latest state for it just latest hash + const res = await sbp('chelonia/out/latestHash', state.attributes.mailbox) + assertEquals(res, bob.mailbox.hash()) + }) + + await t.step("Should invite Bob to Alice's group", function (done) { + const mailbox = users.bob.mailbox + return new Promise((resolve, reject) => { + sbp('chelonia/out/actionEncrypted', { + action: 'gi.contracts/mailbox/postMessage', + data: { + from: users.bob.decryptedValue().data.attributes.username, + messageType: MAIL_TYPE_MESSAGE, + message: groups.group1.contractID() + }, + contractID: mailbox.contractID(), + hooks: { + prepublish (invite: GIMessage) { + sbp('okTurtles.events/once', invite.hash(), (contractID: string, entry: GIMessage) => { + console.debug('Bob successfully got invite!') + assertEquals(entry.decryptedValue().data.message, groups.group1.contractID()) + resolve() + }) + } + } + }) + }) + }) + + await t.step('Should post an event', function () { + return createPaymentTo(users.bob, 100, groups.group1.contractID()) + }) + + await t.step('Should sync group and verify payments in state', async function () { + await sbp('chelonia/contract/sync', groups.group1.contractID()) + assertEquals(Object.keys(vuexState[groups.group1.contractID()].payments).length, 1) + }) + + await t.step('Should fail with wrong contractID', async function () { + try { + await createPaymentTo(users.bob, 100, '') + return Promise.reject(new Error("shouldn't get here!")) + } catch (e) { + return Promise.resolve() + } + }) + + // TODO: these events, as well as all messages sent over the sockets + // should all be authenticated and identified by the user's + // identity contract + }) + + await tests.step('File upload', async function (t) { + await t.step('Should upload "avatar-default.png" as "multipart/form-data"', async function () { + const form = new FormData() + const filepath = './frontend/assets/images/user-avatar-default.png' + // const context = blake2bInit(32, null) + // const stream = fs.createReadStream(filepath) + // // the problem here is that we need to get the hash of the file + // // but doing so consumes the stream, invalidating it and making it + // // so that we can't simply do `form.append('data', stream)` + // // I tried creating a secondary piped stream and sending that instead, + // // however that didn't work. + // // const pass = new PassThrough() // couldn't get this or Readable to work no matter how I tried + // // So instead we save the raw buffer and send that, using a hack + // // to work around a weird bug in hapi or form-data where we have to + // // specify the filename or otherwise the backend treats the data as a string, + // // resulting in the wrong hash for some reason. By specifying `filename` the backend + // // treats it as a Buffer, and we get the correct file hash. + // // We could of course simply read the file twice, but that seems even more hackish. + // var buffer + // const hash = await new Promise((resolve, reject) => { + // stream.on('error', e => reject(e)) + // stream.on('data', chunk => { + // buffer = buffer ? Buffer.concat([buffer, chunk]) : chunk + // blake2bUpdate(context, chunk) + // }) + // stream.on('end', () => { + // const uint8array = blake2bFinal(context) + // resolve(multihash.toB58String(multihash.encode(Buffer.from(uint8array.buffer), 'blake2b-32', 32))) + // }) + // }) + // since we're just saving the buffer now, we might as well use the simpler readFileSync API + const buffer = Deno.readFileSync(filepath) + const hash = blake32Hash(buffer) + console.log(`hash for ${pathlib.basename(filepath)}: ${hash}`) + form.append('hash', hash) + const blob = new Blob([buffer]) + form.append('data', blob, pathlib.basename(filepath)) + await fetch(`${process.env.API_URL}/file`, { method: 'POST', body: form }) + .then(handleFetchResult('text')) + .then(r => assertEquals(r, `/file/${hash}`)) + }) + }) + + await tests.step('Cleanup', async function (t) { + await t.step('Should destroy all opened sockets', function () { + // The code below was originally Object.values(...) but changed to .keys() + // due to a similar flow issue to https://github.com/facebook/flow/issues/2221 + Object.keys(users).forEach((userKey) => { + users[userKey].socket && users[userKey].socket.destroy() + }) + }) + }) + }, + sanitizeResources: false, + sanitizeOps: false, +}) + +// Potentially useful for dealing with fetch API: +// function streamToPromise (stream, dataHandler) { +// return new Promise((resolve, reject) => { +// stream.on('data', (...args) => { +// try { dataHandler(...args) } catch (err) { reject(err) } +// }) +// stream.on('end', resolve) +// stream.on('error', reject) +// }) +// } +// see: https://github.com/bitinn/node-fetch/blob/master/test/test.js +// This used to be in the 'Should get mailbox info for Bob' test, before the +// server manually created a JSON array out of the objects being streamed. +// await streamToPromise(res.body, chunk => { +// console.log(bold.red('CHUNK:'), chunk.toString()) +// events.push(JSON.parse(chunk.toString())) +// }) diff --git a/test/common/common.js b/test/common/common.js new file mode 100644 index 0000000000..71da9f43fc --- /dev/null +++ b/test/common/common.js @@ -0,0 +1,302 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; + +// frontend/common/common.js +import { default as default2 } from "vue"; + +// node_modules/@sbp/sbp/dist/module.mjs +var selectors = {}; +var domains = {}; +var globalFilters = []; +var domainFilters = {}; +var selectorFilters = {}; +var unsafeSelectors = {}; +var DOMAIN_REGEX = /^[^/]+/; +function sbp(selector, ...data) { + const domain = domainFromSelector(selector); + if (!selectors[selector]) { + throw new Error(`SBP: selector not registered: ${selector}`); + } + for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) { + if (filters) { + for (const filter of filters) { + if (filter(domain, selector, data) === false) + return; + } + } + } + return selectors[selector].call(domains[domain].state, ...data); +} +function domainFromSelector(selector) { + const domainLookup = DOMAIN_REGEX.exec(selector); + if (domainLookup === null) { + throw new Error(`SBP: selector missing domain: ${selector}`); + } + return domainLookup[0]; +} +var SBP_BASE_SELECTORS = { + "sbp/selectors/register": function(sels) { + const registered = []; + for (const selector in sels) { + const domain = domainFromSelector(selector); + if (selectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`); + } else if (typeof sels[selector] === "function") { + if (unsafeSelectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`); + } + const fn = selectors[selector] = sels[selector]; + registered.push(selector); + if (!domains[domain]) { + domains[domain] = { state: {} }; + } + if (selector === `${domain}/_init`) { + fn.call(domains[domain].state); + } + } + } + return registered; + }, + "sbp/selectors/unregister": function(sels) { + for (const selector of sels) { + if (!unsafeSelectors[selector]) { + throw new Error(`SBP: can't unregister locked selector: ${selector}`); + } + delete selectors[selector]; + } + }, + "sbp/selectors/overwrite": function(sels) { + sbp("sbp/selectors/unregister", Object.keys(sels)); + return sbp("sbp/selectors/register", sels); + }, + "sbp/selectors/unsafe": function(sels) { + for (const selector of sels) { + if (selectors[selector]) { + throw new Error("unsafe must be called before registering selector"); + } + unsafeSelectors[selector] = true; + } + }, + "sbp/selectors/lock": function(sels) { + for (const selector of sels) { + delete unsafeSelectors[selector]; + } + }, + "sbp/selectors/fn": function(sel) { + return selectors[sel]; + }, + "sbp/filters/global/add": function(filter) { + globalFilters.push(filter); + }, + "sbp/filters/domain/add": function(domain, filter) { + if (!domainFilters[domain]) + domainFilters[domain] = []; + domainFilters[domain].push(filter); + }, + "sbp/filters/selector/add": function(selector, filter) { + if (!selectorFilters[selector]) + selectorFilters[selector] = []; + selectorFilters[selector].push(filter); + } +}; +SBP_BASE_SELECTORS["sbp/selectors/register"](SBP_BASE_SELECTORS); +var module_default = sbp; + +// frontend/common/vSafeHtml.js +import dompurify from "dompurify"; +import Vue from "vue"; + +// frontend/model/contracts/shared/giLodash.js +function cloneDeep(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +// frontend/common/vSafeHtml.js +var defaultConfig = { + ALLOWED_ATTR: ["class"], + ALLOWED_TAGS: ["b", "br", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: true +}; +var transform = (el, binding) => { + if (binding.oldValue !== binding.value) { + let config = defaultConfig; + if (binding.arg === "a") { + config = cloneDeep(config); + config.ALLOWED_ATTR.push("href", "target"); + config.ALLOWED_TAGS.push("a"); + } + el.textContent = ""; + el.appendChild(dompurify.sanitize(binding.value, config)); + } +}; +Vue.directive("safe-html", { + bind: transform, + update: transform +}); + +// frontend/common/translations.js +import dompurify2 from "dompurify"; +import Vue2 from "vue"; + +// frontend/common/stringTemplate.js +var nargs = /\{([0-9a-zA-Z_]+)\}/g; +function template(string, ...args) { + const firstArg = args[0]; + const replacementsByKey = typeof firstArg === "object" && firstArg !== null ? firstArg : args; + return string.replace(nargs, function replaceArg(match, capture, index) { + if (string[index - 1] === "{" && string[index + match.length] === "}") { + return capture; + } + const maybeReplacement = Object.prototype.hasOwnProperty.call(replacementsByKey, capture) ? replacementsByKey[capture] : void 0; + if (maybeReplacement === null || maybeReplacement === void 0) { + return ""; + } + return String(maybeReplacement); + }); +} + +// frontend/common/translations.js +Vue2.prototype.L = L; +Vue2.prototype.LTags = LTags; +var defaultLanguage = "en-US"; +var defaultLanguageCode = "en"; +var defaultTranslationTable = {}; +var dompurifyConfig = { + ...defaultConfig, + ALLOWED_ATTR: ["class", "href", "rel", "target"], + ALLOWED_TAGS: ["a", "b", "br", "button", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: false +}; +var currentLanguage = defaultLanguage; +var currentLanguageCode = defaultLanguage.split("-")[0]; +var currentTranslationTable = defaultTranslationTable; +module_default("sbp/selectors/register", { + "translations/init": async function init(language) { + const [languageCode] = language.toLowerCase().split("-"); + if (language.toLowerCase() === currentLanguage.toLowerCase()) + return; + if (languageCode === currentLanguageCode) + return; + if (languageCode === defaultLanguageCode) { + currentLanguage = defaultLanguage; + currentLanguageCode = defaultLanguageCode; + currentTranslationTable = defaultTranslationTable; + return; + } + try { + currentTranslationTable = await module_default("backend/translations/get", language) || defaultTranslationTable; + currentLanguage = language; + currentLanguageCode = languageCode; + } catch (error) { + console.error(error); + } + } +}); +function LTags(...tags) { + const o = { + "br_": "
" + }; + for (const tag of tags) { + o[`${tag}_`] = `<${tag}>`; + o[`_${tag}`] = ``; + } + return o; +} +function L(key, args) { + return template(currentTranslationTable[key] || key, args).replace(/\s(?=[;:?!])/g, " "); +} +function LError(error) { + let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`; + if (!module_default("state/vuex/state").loggedIn) { + url = "https://github.com/okTurtles/group-income/issues"; + } + return { + reportError: L('"{errorMsg}". You can {a_}report the error{_a}.', { + errorMsg: error.message, + "a_": ``, + "_a": "" + }) + }; +} +function sanitize(inputString) { + return dompurify2.sanitize(inputString, dompurifyConfig); +} +Vue2.component("i18n", { + functional: true, + props: { + args: [Object, Array], + tag: { + type: String, + default: "span" + }, + compile: Boolean + }, + render: function(h, context) { + const text = context.children[0].text; + const translation = L(text, context.props.args || {}); + if (!translation) { + console.warn("The following i18n text was not translated correctly:", text); + return h(context.props.tag, context.data, text); + } + if (context.props.tag === "a" && context.data.attrs.target === "_blank") { + context.data.attrs.rel = "noopener noreferrer"; + } + if (context.props.compile) { + const result = Vue2.compile("" + sanitize(translation) + ""); + return result.render.call({ + _c: (tag, ...args) => { + if (tag === "wrap") { + return h(context.props.tag, context.data, ...args); + } else { + return h(tag, ...args); + } + }, + _v: (x) => x + }); + } else { + if (!context.data.domProps) + context.data.domProps = {}; + context.data.domProps.innerHTML = sanitize(translation); + return h(context.props.tag, context.data); + } + } +}); + +// frontend/common/errors.js +var errors_exports = {}; +__export(errors_exports, { + GIErrorIgnoreAndBan: () => GIErrorIgnoreAndBan, + GIErrorUIRuntimeError: () => GIErrorUIRuntimeError +}); +var GIErrorIgnoreAndBan = class extends Error { + constructor(...params) { + super(...params); + this.name = "GIErrorIgnoreAndBan"; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +}; +var GIErrorUIRuntimeError = class extends Error { + constructor(...params) { + super(...params); + this.name = "GIErrorUIRuntimeError"; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +}; +export { + errors_exports as Errors, + GIErrorIgnoreAndBan, + GIErrorUIRuntimeError, + L, + LError, + LTags, + default2 as Vue +}; +//# sourceMappingURL=common.js.map diff --git a/test/common/common.js.map b/test/common/common.js.map new file mode 100644 index 0000000000..f74b53c7bc --- /dev/null +++ b/test/common/common.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../frontend/common/common.js", "../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/common/errors.js"], + "sourcesContent": ["'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nexport class GIErrorIgnoreAndBan extends Error {\n // ugly boilerplate because JavaScript is stupid\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types\n constructor (...params ) {\n super(...params)\n this.name = 'GIErrorIgnoreAndBan'\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n\n// Used to throw human readable errors on UI.\nexport class GIErrorUIRuntimeError extends Error {\n constructor (...params ) {\n super(...params)\n // this.name = this.constructor.name\n this.name = 'GIErrorUIRuntimeError' // string literal so minifier doesn't overwrite\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n"], + "mappings": ";;;;;;;;AAwBA;;;ACnBA,IAAM,YAAY,CAAC;AACnB,IAAM,UAAU,CAAC;AACjB,IAAM,gBAAgB,CAAC;AACvB,IAAM,gBAAgB,CAAC;AACvB,IAAM,kBAAkB,CAAC;AACzB,IAAM,kBAAkB,CAAC;AAEzB,IAAM,eAAe;AAErB,aAAc,aAAa,MAAM;AAC/B,QAAM,SAAS,mBAAmB,QAAQ;AAC1C,MAAI,CAAC,UAAU,WAAW;AACxB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AAGA,aAAW,WAAW,CAAC,gBAAgB,WAAW,cAAc,SAAS,aAAa,GAAG;AACvF,QAAI,SAAS;AACX,iBAAW,UAAU,SAAS;AAC5B,YAAI,OAAO,QAAQ,UAAU,IAAI,MAAM;AAAO;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACA,SAAO,UAAU,UAAU,KAAK,QAAQ,QAAQ,OAAO,GAAG,IAAI;AAChE;AAEO,4BAA6B,UAAU;AAC5C,QAAM,eAAe,aAAa,KAAK,QAAQ;AAC/C,MAAI,iBAAiB,MAAM;AACzB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AACA,SAAO,aAAa;AACtB;AAEA,IAAM,qBAAqB;AAAA,EACzB,0BAA0B,SAAU,MAAM;AACxC,UAAM,aAAa,CAAC;AACpB,eAAW,YAAY,MAAM;AAC3B,YAAM,SAAS,mBAAmB,QAAQ;AAC1C,UAAI,UAAU,WAAW;AACvB,QAAC,SAAQ,QAAQ,QAAQ,KAAK,6DAA6D,WAAW;AAAA,MACxG,WAAW,OAAO,KAAK,cAAc,YAAY;AAC/C,YAAI,gBAAgB,WAAW;AAE7B,UAAC,SAAQ,QAAQ,QAAQ,KAAK,6CAA6C,gDAAgD;AAAA,QAC7H;AACA,cAAM,KAAK,UAAU,YAAY,KAAK;AACtC,mBAAW,KAAK,QAAQ;AAExB,YAAI,CAAC,QAAQ,SAAS;AACpB,kBAAQ,UAAU,EAAE,OAAO,CAAC,EAAE;AAAA,QAChC;AAEA,YAAI,aAAa,GAAG,gBAAgB;AAClC,aAAG,KAAK,QAAQ,QAAQ,KAAK;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,4BAA4B,SAAU,MAAM;AAC1C,eAAW,YAAY,MAAM;AAC3B,UAAI,CAAC,gBAAgB,WAAW;AAC9B,cAAM,IAAI,MAAM,0CAA0C,UAAU;AAAA,MACtE;AACA,aAAO,UAAU;AAAA,IACnB;AAAA,EACF;AAAA,EACA,2BAA2B,SAAU,MAAM;AACzC,QAAI,4BAA4B,OAAO,KAAK,IAAI,CAAC;AACjD,WAAO,IAAI,0BAA0B,IAAI;AAAA,EAC3C;AAAA,EACA,wBAAwB,SAAU,MAAM;AACtC,eAAW,YAAY,MAAM;AAC3B,UAAI,UAAU,WAAW;AACvB,cAAM,IAAI,MAAM,mDAAmD;AAAA,MACrE;AACA,sBAAgB,YAAY;AAAA,IAC9B;AAAA,EACF;AAAA,EACA,sBAAsB,SAAU,MAAM;AACpC,eAAW,YAAY,MAAM;AAC3B,aAAO,gBAAgB;AAAA,IACzB;AAAA,EACF;AAAA,EACA,oBAAoB,SAAU,KAAK;AACjC,WAAO,UAAU;AAAA,EACnB;AAAA,EACA,0BAA0B,SAAU,QAAQ;AAC1C,kBAAc,KAAK,MAAM;AAAA,EAC3B;AAAA,EACA,0BAA0B,SAAU,QAAQ,QAAQ;AAClD,QAAI,CAAC,cAAc;AAAS,oBAAc,UAAU,CAAC;AACrD,kBAAc,QAAQ,KAAK,MAAM;AAAA,EACnC;AAAA,EACA,4BAA4B,SAAU,UAAU,QAAQ;AACtD,QAAI,CAAC,gBAAgB;AAAW,sBAAgB,YAAY,CAAC;AAC7D,oBAAgB,UAAU,KAAK,MAAM;AAAA,EACvC;AACF;AAEA,mBAAmB,0BAA0B,kBAAkB;AAE/D,IAAO,iBAAQ;;;AC1Ff;AACA;;;ACwBO,mBAAoB,KAAK;AAC9B,SAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;AACvC;;;ADtBO,IAAM,gBAAgB;AAAA,EAC3B,cAAc,CAAC,OAAO;AAAA,EACtB,cAAc,CAAC,KAAK,MAAM,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EAEtF,qBAAqB;AACvB;AAEA,IAAM,YAAY,CAAC,IAAI,YAAY;AACjC,MAAI,QAAQ,aAAa,QAAQ,OAAO;AACtC,QAAI,SAAS;AACb,QAAI,QAAQ,QAAQ,KAAK;AACvB,eAAS,UAAU,MAAM;AACzB,aAAO,aAAa,KAAK,QAAQ,QAAQ;AACzC,aAAO,aAAa,KAAK,GAAG;AAAA,IAC9B;AACA,OAAG,cAAc;AACjB,OAAG,YAAY,UAAU,SAAS,QAAQ,OAAO,MAAM,CAAC;AAAA,EAC1D;AACF;AAWA,IAAI,UAAU,aAAa;AAAA,EACzB,MAAM;AAAA,EACN,QAAQ;AACV,CAAC;;;AElDD;AACA;;;ACNA,IAAM,QAAQ;AAEC,kBAAmB,WAAmB,MAAqB;AACxE,QAAM,WAAW,KAAK;AAGtB,QAAM,oBACH,OAAO,aAAa,YAAY,aAAa,OAC1C,WACA;AAGN,SAAO,OAAO,QAAQ,OAAO,oBAAqB,OAAO,SAAS,OAAO;AAEvE,QAAI,OAAO,QAAQ,OAAO,OAAO,OAAO,QAAQ,MAAM,YAAY,KAAK;AACrE,aAAO;AAAA,IACT;AAEA,UAAM,mBAGJ,OAAO,UAAU,eAAe,KAAK,mBAAmB,OAAO,IAC3D,kBAAkB,WAClB;AAGN,QAAI,qBAAqB,QAAQ,qBAAqB,QAAW;AAC/D,aAAO;AAAA,IACT;AACA,WAAO,OAAO,gBAAgB;AAAA,EAChC,CAAC;AACH;;;ADtBA,KAAI,UAAU,IAAI;AAClB,KAAI,UAAU,QAAQ;AAEtB,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,0BAAgD,CAAC;AAOvD,IAAM,kBAAkB;AAAA,EACtB,GAAG;AAAA,EACH,cAAc,CAAC,SAAS,QAAQ,OAAO,QAAQ;AAAA,EAC/C,cAAc,CAAC,KAAK,KAAK,MAAM,UAAU,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EACrG,qBAAqB;AACvB;AAEA,IAAI,kBAAkB;AACtB,IAAI,sBAAsB,gBAAgB,MAAM,GAAG,EAAE;AACrD,IAAI,0BAA0B;AAY9B,eAAI,0BAA0B;AAAA,EAC5B,qBAAqB,oBAAqB,UAAiC;AAEzE,UAAM,CAAC,gBAAgB,SAAS,YAAY,EAAE,MAAM,GAAG;AAGvD,QAAI,SAAS,YAAY,MAAM,gBAAgB,YAAY;AAAG;AAI9D,QAAI,iBAAiB;AAAqB;AAG1C,QAAI,iBAAiB,qBAAqB;AACxC,wBAAkB;AAClB,4BAAsB;AACtB,gCAA0B;AAC1B;AAAA,IACF;AACA,QAAI;AACF,gCAA2B,MAAM,eAAI,4BAA4B,QAAQ,KAAM;AAG/E,wBAAkB;AAClB,4BAAsB;AAAA,IACxB,SAAS,OAAP;AACA,cAAQ,MAAM,KAAK;AAAA,IACrB;AAAA,EACF;AACF,CAAC;AA0CM,kBAAmB,MAAiC;AACzD,QAAM,IAAI;AAAA,IACR,OAAO;AAAA,EACT;AACA,aAAW,OAAO,MAAM;AACtB,MAAE,GAAG,UAAU,IAAI;AACnB,MAAE,IAAI,SAAS,KAAK;AAAA,EACtB;AACA,SAAO;AACT;AAEe,WACb,KACA,MACQ;AACR,SAAO,SAAS,wBAAwB,QAAQ,KAAK,IAAI,EAEtD,QAAQ,iBAAiB,QAAQ;AACtC;AAEO,gBAAiB,OAAoC;AAC1D,MAAI,MAAM,4EAA4E,UAAU,MAAM,OAAO;AAC7G,MAAI,CAAC,eAAI,kBAAkB,EAAE,UAAU;AACrC,UAAM;AAAA,EACR;AACA,SAAO;AAAA,IACL,aAAa,EAAE,mDAAmD;AAAA,MAChE,UAAU,MAAM;AAAA,MAChB,MAAM,yCAAyC;AAAA,MAC/C,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,kBAAmB,aAAa;AAC9B,SAAO,WAAU,SAAS,aAAa,eAAe;AACxD;AAEA,KAAI,UAAU,QAAQ;AAAA,EACpB,YAAY;AAAA,EACZ,OAAO;AAAA,IACL,MAAM,CAAC,QAAQ,KAAK;AAAA,IACpB,KAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA,SAAS;AAAA,EACX;AAAA,EACA,QAAQ,SAAU,GAAG,SAAS;AAC5B,UAAM,OAAO,QAAQ,SAAS,GAAG;AACjC,UAAM,cAAc,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,CAAC;AACpD,QAAI,CAAC,aAAa;AAChB,cAAQ,KAAK,yDAAyD,IAAI;AAC1E,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,IAAI;AAAA,IAChD;AAEA,QAAI,QAAQ,MAAM,QAAQ,OAAO,QAAQ,KAAK,MAAM,WAAW,UAAU;AACvE,cAAQ,KAAK,MAAM,MAAM;AAAA,IAC3B;AACA,QAAI,QAAQ,MAAM,SAAS;AACzB,YAAM,SAAS,KAAI,QAAQ,WAAW,SAAS,WAAW,IAAI,SAAS;AAEvE,aAAO,OAAO,OAAO,KAAK;AAAA,QACxB,IAAI,CAAC,QAAQ,SAAS;AACpB,cAAI,QAAQ,QAAQ;AAClB,mBAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,GAAG,IAAI;AAAA,UACnD,OAAO;AACL,mBAAO,EAAE,KAAK,GAAG,IAAI;AAAA,UACvB;AAAA,QACF;AAAA,QACA,IAAI,OAAK;AAAA,MACX,CAAC;AAAA,IACH,OAAO;AACL,UAAI,CAAC,QAAQ,KAAK;AAAU,gBAAQ,KAAK,WAAW,CAAC;AACrD,cAAQ,KAAK,SAAS,YAAY,SAAS,WAAW;AACtD,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,IAAI;AAAA,IAC1C;AAAA,EACF;AACF,CAAC;;;AE/LD;AAAA;AAAA;AAAA;AAAA;AAEO,IAAM,sBAAN,cAAkC,MAAM;AAAA,EAG7C,eAAgB,QAAe;AAC7B,UAAM,GAAG,MAAM;AACf,SAAK,OAAO;AACZ,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,KAAK,WAAW;AAAA,IAChD;AAAA,EACF;AACF;AAGO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC/C,eAAgB,QAAe;AAC7B,UAAM,GAAG,MAAM;AAEf,SAAK,OAAO;AACZ,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,KAAK,WAAW;AAAA,IAChD;AAAA,EACF;AACF;", + "names": [] +} diff --git a/test/contracts/chatroom.js b/test/contracts/chatroom.js new file mode 100644 index 0000000000..c2fbe19935 --- /dev/null +++ b/test/contracts/chatroom.js @@ -0,0 +1,1001 @@ +"use strict"; + +// node_modules/@sbp/sbp/dist/module.mjs +var selectors = {}; +var domains = {}; +var globalFilters = []; +var domainFilters = {}; +var selectorFilters = {}; +var unsafeSelectors = {}; +var DOMAIN_REGEX = /^[^/]+/; +function sbp(selector, ...data) { + const domain = domainFromSelector(selector); + if (!selectors[selector]) { + throw new Error(`SBP: selector not registered: ${selector}`); + } + for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) { + if (filters) { + for (const filter of filters) { + if (filter(domain, selector, data) === false) + return; + } + } + } + return selectors[selector].call(domains[domain].state, ...data); +} +function domainFromSelector(selector) { + const domainLookup = DOMAIN_REGEX.exec(selector); + if (domainLookup === null) { + throw new Error(`SBP: selector missing domain: ${selector}`); + } + return domainLookup[0]; +} +var SBP_BASE_SELECTORS = { + "sbp/selectors/register": function(sels) { + const registered = []; + for (const selector in sels) { + const domain = domainFromSelector(selector); + if (selectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`); + } else if (typeof sels[selector] === "function") { + if (unsafeSelectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`); + } + const fn = selectors[selector] = sels[selector]; + registered.push(selector); + if (!domains[domain]) { + domains[domain] = { state: {} }; + } + if (selector === `${domain}/_init`) { + fn.call(domains[domain].state); + } + } + } + return registered; + }, + "sbp/selectors/unregister": function(sels) { + for (const selector of sels) { + if (!unsafeSelectors[selector]) { + throw new Error(`SBP: can't unregister locked selector: ${selector}`); + } + delete selectors[selector]; + } + }, + "sbp/selectors/overwrite": function(sels) { + sbp("sbp/selectors/unregister", Object.keys(sels)); + return sbp("sbp/selectors/register", sels); + }, + "sbp/selectors/unsafe": function(sels) { + for (const selector of sels) { + if (selectors[selector]) { + throw new Error("unsafe must be called before registering selector"); + } + unsafeSelectors[selector] = true; + } + }, + "sbp/selectors/lock": function(sels) { + for (const selector of sels) { + delete unsafeSelectors[selector]; + } + }, + "sbp/selectors/fn": function(sel) { + return selectors[sel]; + }, + "sbp/filters/global/add": function(filter) { + globalFilters.push(filter); + }, + "sbp/filters/domain/add": function(domain, filter) { + if (!domainFilters[domain]) + domainFilters[domain] = []; + domainFilters[domain].push(filter); + }, + "sbp/filters/selector/add": function(selector, filter) { + if (!selectorFilters[selector]) + selectorFilters[selector] = []; + selectorFilters[selector].push(filter); + } +}; +SBP_BASE_SELECTORS["sbp/selectors/register"](SBP_BASE_SELECTORS); +var module_default = sbp; + +// frontend/common/common.js +import { default as default2 } from "vue"; + +// frontend/common/vSafeHtml.js +import dompurify from "dompurify"; +import Vue from "vue"; + +// frontend/model/contracts/shared/giLodash.js +function cloneDeep(obj) { + return JSON.parse(JSON.stringify(obj)); +} +function isMergeableObject(val) { + const nonNullObject = val && typeof val === "object"; + return nonNullObject && Object.prototype.toString.call(val) !== "[object RegExp]" && Object.prototype.toString.call(val) !== "[object Date]"; +} +function merge(obj, src) { + for (const key in src) { + const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : void 0; + if (clone && isMergeableObject(obj[key])) { + merge(obj[key], clone); + continue; + } + obj[key] = clone || src[key]; + } + return obj; +} + +// frontend/common/vSafeHtml.js +var defaultConfig = { + ALLOWED_ATTR: ["class"], + ALLOWED_TAGS: ["b", "br", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: true +}; +var transform = (el, binding) => { + if (binding.oldValue !== binding.value) { + let config = defaultConfig; + if (binding.arg === "a") { + config = cloneDeep(config); + config.ALLOWED_ATTR.push("href", "target"); + config.ALLOWED_TAGS.push("a"); + } + el.textContent = ""; + el.appendChild(dompurify.sanitize(binding.value, config)); + } +}; +Vue.directive("safe-html", { + bind: transform, + update: transform +}); + +// frontend/common/translations.js +import dompurify2 from "dompurify"; +import Vue2 from "vue"; + +// frontend/common/stringTemplate.js +var nargs = /\{([0-9a-zA-Z_]+)\}/g; +function template(string3, ...args) { + const firstArg = args[0]; + const replacementsByKey = typeof firstArg === "object" && firstArg !== null ? firstArg : args; + return string3.replace(nargs, function replaceArg(match, capture, index) { + if (string3[index - 1] === "{" && string3[index + match.length] === "}") { + return capture; + } + const maybeReplacement = Object.prototype.hasOwnProperty.call(replacementsByKey, capture) ? replacementsByKey[capture] : void 0; + if (maybeReplacement === null || maybeReplacement === void 0) { + return ""; + } + return String(maybeReplacement); + }); +} + +// frontend/common/translations.js +Vue2.prototype.L = L; +Vue2.prototype.LTags = LTags; +var defaultLanguage = "en-US"; +var defaultLanguageCode = "en"; +var defaultTranslationTable = {}; +var dompurifyConfig = { + ...defaultConfig, + ALLOWED_ATTR: ["class", "href", "rel", "target"], + ALLOWED_TAGS: ["a", "b", "br", "button", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: false +}; +var currentLanguage = defaultLanguage; +var currentLanguageCode = defaultLanguage.split("-")[0]; +var currentTranslationTable = defaultTranslationTable; +module_default("sbp/selectors/register", { + "translations/init": async function init(language) { + const [languageCode] = language.toLowerCase().split("-"); + if (language.toLowerCase() === currentLanguage.toLowerCase()) + return; + if (languageCode === currentLanguageCode) + return; + if (languageCode === defaultLanguageCode) { + currentLanguage = defaultLanguage; + currentLanguageCode = defaultLanguageCode; + currentTranslationTable = defaultTranslationTable; + return; + } + try { + currentTranslationTable = await module_default("backend/translations/get", language) || defaultTranslationTable; + currentLanguage = language; + currentLanguageCode = languageCode; + } catch (error) { + console.error(error); + } + } +}); +function LTags(...tags) { + const o = { + "br_": "
" + }; + for (const tag of tags) { + o[`${tag}_`] = `<${tag}>`; + o[`_${tag}`] = ``; + } + return o; +} +function L(key, args) { + return template(currentTranslationTable[key] || key, args).replace(/\s(?=[;:?!])/g, " "); +} +function sanitize(inputString) { + return dompurify2.sanitize(inputString, dompurifyConfig); +} +Vue2.component("i18n", { + functional: true, + props: { + args: [Object, Array], + tag: { + type: String, + default: "span" + }, + compile: Boolean + }, + render: function(h, context) { + const text = context.children[0].text; + const translation = L(text, context.props.args || {}); + if (!translation) { + console.warn("The following i18n text was not translated correctly:", text); + return h(context.props.tag, context.data, text); + } + if (context.props.tag === "a" && context.data.attrs.target === "_blank") { + context.data.attrs.rel = "noopener noreferrer"; + } + if (context.props.compile) { + const result = Vue2.compile("" + sanitize(translation) + ""); + return result.render.call({ + _c: (tag, ...args) => { + if (tag === "wrap") { + return h(context.props.tag, context.data, ...args); + } else { + return h(tag, ...args); + } + }, + _v: (x) => x + }); + } else { + if (!context.data.domProps) + context.data.domProps = {}; + context.data.domProps.innerHTML = sanitize(translation); + return h(context.props.tag, context.data); + } + } +}); + +// frontend/model/contracts/shared/constants.js +var CHATROOM_NAME_LIMITS_IN_CHARS = 50; +var CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280; +var CHATROOM_ACTIONS_PER_PAGE = 40; +var CHATROOM_MESSAGES_PER_PAGE = 20; +var CHATROOM_MESSAGE_ACTION = "chatroom-message-action"; +var MESSAGE_RECEIVE = "message-receive"; +var CHATROOM_TYPES = { + INDIVIDUAL: "individual", + GROUP: "group" +}; +var CHATROOM_PRIVACY_LEVEL = { + GROUP: "chatroom-privacy-level-group", + PRIVATE: "chatroom-privacy-level-private", + PUBLIC: "chatroom-privacy-level-public" +}; +var MESSAGE_TYPES = { + POLL: "message-poll", + TEXT: "message-text", + INTERACTIVE: "message-interactive", + NOTIFICATION: "message-notification" +}; +var MESSAGE_NOTIFICATIONS = { + ADD_MEMBER: "add-member", + JOIN_MEMBER: "join-member", + LEAVE_MEMBER: "leave-member", + KICK_MEMBER: "kick-member", + UPDATE_DESCRIPTION: "update-description", + UPDATE_NAME: "update-name", + DELETE_CHANNEL: "delete-channel", + VOTE: "vote" +}; +var MAIL_TYPE_MESSAGE = "message"; +var MAIL_TYPE_FRIEND_REQ = "friend-request"; + +// frontend/model/contracts/misc/flowTyper.js +var EMPTY_VALUE = Symbol("@@empty"); +var isEmpty = (v) => v === EMPTY_VALUE; +var isNil = (v) => v === null; +var isUndef = (v) => typeof v === "undefined"; +var isBoolean = (v) => typeof v === "boolean"; +var isNumber = (v) => typeof v === "number"; +var isString = (v) => typeof v === "string"; +var isObject = (v) => !isNil(v) && typeof v === "object"; +var isFunction = (v) => typeof v === "function"; +var getType = (typeFn, _options) => { + if (isFunction(typeFn.type)) + return typeFn.type(_options); + return typeFn.name || "?"; +}; +var TypeValidatorError = class extends Error { + expectedType; + valueType; + value; + typeScope; + sourceFile; + constructor(message, expectedType, valueType, value, typeName = "", typeScope = "") { + const errMessage = message || `invalid "${valueType}" value type; ${typeName || expectedType} type expected`; + super(errMessage); + this.expectedType = expectedType; + this.valueType = valueType; + this.value = value; + this.typeScope = typeScope || ""; + this.sourceFile = this.getSourceFile(); + this.message = `${errMessage} +${this.getErrorInfo()}`; + this.name = this.constructor.name; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, TypeValidatorError); + } + } + getSourceFile() { + const fileNames = this.stack.match(/(\/[\w_\-.]+)+(\.\w+:\d+:\d+)/g) || []; + return fileNames.find((fileName) => fileName.indexOf("/flowTyper-js/dist/") === -1) || ""; + } + getErrorInfo() { + return ` + file ${this.sourceFile} + scope ${this.typeScope} + expected ${this.expectedType.replace(/\n/g, "")} + type ${this.valueType} + value ${this.value} +`; + } +}; +var validatorError = (typeFn, value, scope, message, expectedType, valueType) => { + return new TypeValidatorError(message, expectedType || getType(typeFn), valueType || typeof value, JSON.stringify(value), typeFn.name, scope); +}; +var arrayOf = (typeFn, _scope = "Array") => { + function array(value) { + if (isEmpty(value)) + return [typeFn(value)]; + if (Array.isArray(value)) { + let index = 0; + return value.map((v) => typeFn(v, `${_scope}[${index++}]`)); + } + throw validatorError(array, value, _scope); + } + array.type = () => `Array<${getType(typeFn)}>`; + return array; +}; +var literalOf = (primitive) => { + function literal(value, _scope = "") { + if (isEmpty(value) || value === primitive) + return primitive; + throw validatorError(literal, value, _scope); + } + literal.type = () => { + if (isBoolean(primitive)) + return `${primitive ? "true" : "false"}`; + else + return `"${primitive}"`; + }; + return literal; +}; +var mapOf = (keyTypeFn, typeFn) => { + function mapOf2(value) { + if (isEmpty(value)) + return {}; + const o = object(value); + const reducer = (acc, key) => Object.assign(acc, { + [keyTypeFn(key, "Map[_]")]: typeFn(o[key], `Map.${key}`) + }); + return Object.keys(o).reduce(reducer, {}); + } + mapOf2.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`; + return mapOf2; +}; +var object = function(value) { + if (isEmpty(value)) + return {}; + if (isObject(value) && !Array.isArray(value)) { + return Object.assign({}, value); + } + throw validatorError(object, value); +}; +var objectOf = (typeObj, _scope = "Object") => { + function object2(value) { + const o = object(value); + const typeAttrs = Object.keys(typeObj); + const unknownAttr = Object.keys(o).find((attr) => !typeAttrs.includes(attr)); + if (unknownAttr) { + throw validatorError(object2, value, _scope, `missing object property '${unknownAttr}' in ${_scope} type`); + } + const undefAttr = typeAttrs.find((property) => { + const propertyTypeFn = typeObj[property]; + return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + }); + if (undefAttr) { + throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); + } + const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { + const typeFn = typeObj[key]; + if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + return Object.assign(acc, {}); + } else { + return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); + } + }; + return typeAttrs.reduce(reducer, {}); + } + object2.type = () => { + const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + return `{| + ${props.join(",\n ")} +|}`; + }; + return object2; +}; +function objectMaybeOf(validations, _scope = "Object") { + return function(data) { + object(data); + for (const key in data) { + validations[key]?.(data[key], `${_scope}.${key}`); + } + return data; + }; +} +var optional = (typeFn) => { + const unionFn = unionOf(typeFn, undef); + function optional2(v) { + return unionFn(v); + } + optional2.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn); + return optional2; +}; +function undef(value, _scope = "") { + if (isEmpty(value) || isUndef(value)) + return void 0; + throw validatorError(undef, value, _scope); +} +undef.type = () => "void"; +var number = function number2(value, _scope = "") { + if (isEmpty(value)) + return 0; + if (isNumber(value)) + return value; + throw validatorError(number2, value, _scope); +}; +var string = function string2(value, _scope = "") { + if (isEmpty(value)) + return ""; + if (isString(value)) + return value; + throw validatorError(string2, value, _scope); +}; +function unionOf_(...typeFuncs) { + function union(value, _scope = "") { + for (const typeFn of typeFuncs) { + try { + return typeFn(value, _scope); + } catch (_) { + } + } + throw validatorError(union, value, _scope); + } + union.type = () => `(${typeFuncs.map((fn) => getType(fn)).join(" | ")})`; + return union; +} +var unionOf = unionOf_; + +// frontend/model/contracts/shared/types.js +var inviteType = objectOf({ + inviteSecret: string, + quantity: number, + creator: string, + invitee: optional(string), + status: string, + responses: mapOf(string, string), + expires: number +}); +var chatRoomAttributesType = objectOf({ + name: string, + description: string, + type: unionOf(...Object.values(CHATROOM_TYPES).map((v) => literalOf(v))), + privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map((v) => literalOf(v))) +}); +var messageType = objectMaybeOf({ + type: unionOf(...Object.values(MESSAGE_TYPES).map((v) => literalOf(v))), + text: string, + notification: objectMaybeOf({ + type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map((v) => literalOf(v))), + params: mapOf(string, string) + }), + replyingMessage: objectOf({ + id: string, + text: string + }), + emoticons: mapOf(string, arrayOf(string)), + onlyVisibleTo: arrayOf(string) +}); +var mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map((k) => literalOf(k))); + +// frontend/model/contracts/shared/time.js +var MINS_MILLIS = 6e4; +var HOURS_MILLIS = 60 * MINS_MILLIS; +var DAYS_MILLIS = 24 * HOURS_MILLIS; +var MONTHS_MILLIS = 30 * DAYS_MILLIS; + +// frontend/views/utils/misc.js +function logExceptNavigationDuplicated(err) { + err.name !== "NavigationDuplicated" && console.error(err); +} + +// frontend/model/contracts/shared/functions.js +function createMessage({ meta, data, hash, state }) { + const { type, text, replyingMessage } = data; + const { createdDate } = meta; + let newMessage = { + type, + datetime: new Date(createdDate).toISOString(), + id: hash, + from: meta.username + }; + if (type === MESSAGE_TYPES.TEXT) { + newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }; + } else if (type === MESSAGE_TYPES.POLL) { + } else if (type === MESSAGE_TYPES.NOTIFICATION) { + const params = { + channelName: state?.attributes.name, + channelDescription: state?.attributes.description, + ...data.notification + }; + delete params.type; + newMessage = { + ...newMessage, + notification: { type: data.notification.type, params } + }; + } else if (type === MESSAGE_TYPES.INTERACTIVE) { + } + return newMessage; +} +async function leaveChatRoom({ contractID }) { + const rootState = module_default("state/vuex/state"); + const rootGetters = module_default("state/vuex/getters"); + if (contractID === rootGetters.currentChatRoomId) { + module_default("state/vuex/commit", "setCurrentChatRoomId", { + groupId: rootState.currentGroupId + }); + const curRouteName = module_default("controller/router").history.current.name; + if (curRouteName === "GroupChat" || curRouteName === "GroupChatConversation") { + await module_default("controller/router").push({ name: "GroupChatConversation", params: { chatRoomId: rootGetters.currentChatRoomId } }).catch(logExceptNavigationDuplicated); + } + } + module_default("state/vuex/commit", "deleteChatRoomUnread", { chatRoomId: contractID }); + module_default("state/vuex/commit", "deleteChatRoomScrollPosition", { chatRoomId: contractID }); + module_default("chelonia/contract/remove", contractID).catch((e) => { + console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e); + }); +} +function findMessageIdx(id, messages) { + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].id === id) { + return i; + } + } + return -1; +} +function makeMentionFromUsername(username) { + return { + me: `@${username}`, + all: "@all" + }; +} + +// frontend/model/contracts/shared/nativeNotification.js +function makeNotification({ title, body, icon, path }) { + const notificationEnabled = module_default("state/vuex/state").notificationEnabled; + if (typeof Notification === "undefined" || Notification.permission !== "granted" || !notificationEnabled) { + return; + } + const notification = new Notification(title, { body, icon }); + if (path) { + notification.onclick = function(event) { + event.preventDefault(); + module_default("controller/router").push({ path }).catch(console.warn); + }; + } +} + +// frontend/model/contracts/chatroom.js +function createNotificationData(notificationType, moreParams = {}) { + return { + type: MESSAGE_TYPES.NOTIFICATION, + notification: { + type: notificationType, + ...moreParams + } + }; +} +function emitMessageEvent({ contractID, hash }) { + module_default("okTurtles.events/emit", `${CHATROOM_MESSAGE_ACTION}-${contractID}`, { hash }); +} +function addMention({ contractID, messageId, datetime, text, username, chatRoomName }) { + if (module_default("okTurtles.data/get", "READY_TO_JOIN_CHATROOM")) { + return; + } + module_default("state/vuex/commit", "addChatRoomUnreadMention", { + chatRoomId: contractID, + messageId, + createdDate: datetime + }); + const rootGetters = module_default("state/vuex/getters"); + const groupID = rootGetters.groupIdFromChatRoomId(contractID); + const path = `/group-chat/${contractID}`; + makeNotification({ + title: `# ${chatRoomName}`, + body: text, + icon: rootGetters.globalProfile2(groupID, username).picture, + path + }); + module_default("okTurtles.events/emit", MESSAGE_RECEIVE); +} +function deleteMention({ contractID, messageId }) { + module_default("state/vuex/commit", "deleteChatRoomUnreadMention", { chatRoomId: contractID, messageId }); +} +function updateUnreadPosition({ contractID, hash, createdDate }) { + module_default("state/vuex/commit", "setChatRoomUnreadSince", { + chatRoomId: contractID, + messageId: hash, + createdDate + }); +} +module_default("chelonia/defineContract", { + name: "gi.contracts/chatroom", + metadata: { + validate: objectOf({ + createdDate: string, + username: string, + identityContractID: string + }), + create() { + const { username, identityContractID } = module_default("state/vuex/state").loggedIn; + return { + createdDate: new Date().toISOString(), + username, + identityContractID + }; + } + }, + getters: { + currentChatRoomState(state) { + return state; + }, + chatRoomSettings(state, getters) { + return getters.currentChatRoomState.settings || {}; + }, + chatRoomAttributes(state, getters) { + return getters.currentChatRoomState.attributes || {}; + }, + chatRoomUsers(state, getters) { + return getters.currentChatRoomState.users || {}; + }, + chatRoomLatestMessages(state, getters) { + return getters.currentChatRoomState.messages || []; + } + }, + actions: { + "gi.contracts/chatroom": { + validate: objectOf({ + attributes: chatRoomAttributesType + }), + process({ meta, data }, { state }) { + const initialState = merge({ + settings: { + actionsPerPage: CHATROOM_ACTIONS_PER_PAGE, + messagesPerPage: CHATROOM_MESSAGES_PER_PAGE, + maxNameLength: CHATROOM_NAME_LIMITS_IN_CHARS, + maxDescriptionLength: CHATROOM_DESCRIPTION_LIMITS_IN_CHARS + }, + attributes: { + creator: meta.username, + deletedDate: null, + archivedDate: null + }, + users: {}, + messages: [] + }, data); + for (const key in initialState) { + default2.set(state, key, initialState[key]); + } + } + }, + "gi.contracts/chatroom/join": { + validate: objectOf({ + username: string + }), + process({ data, meta, hash }, { state }) { + const { username } = data; + if (!state.saveMessage && state.users[username]) { + console.warn("Can not join the chatroom which you are already part of"); + return; + } + default2.set(state.users, username, { joinedDate: meta.createdDate }); + if (!state.saveMessage) { + return; + } + const notificationType = username === meta.username ? MESSAGE_NOTIFICATIONS.JOIN_MEMBER : MESSAGE_NOTIFICATIONS.ADD_MEMBER; + const notificationData = createNotificationData(notificationType, notificationType === MESSAGE_NOTIFICATIONS.ADD_MEMBER ? { username } : {}); + const newMessage = createMessage({ meta, hash, data: notificationData, state }); + state.messages.push(newMessage); + }, + sideEffect({ contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + if (module_default("okTurtles.data/get", "READY_TO_JOIN_CHATROOM") || module_default("okTurtles.data/get", "JOINING_CHATROOM_ID") === contractID) { + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + } + }, + "gi.contracts/chatroom/rename": { + validate: objectOf({ + name: string + }), + process({ data, meta, hash }, { state }) { + default2.set(state.attributes, "name", data.name); + if (!state.saveMessage) { + return; + } + const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_NAME, {}); + const newMessage = createMessage({ meta, hash, data: notificationData, state }); + state.messages.push(newMessage); + }, + sideEffect({ contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + if (module_default("okTurtles.data/get", "READY_TO_JOIN_CHATROOM")) { + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + } + }, + "gi.contracts/chatroom/changeDescription": { + validate: objectOf({ + description: string + }), + process({ data, meta, hash }, { state }) { + default2.set(state.attributes, "description", data.description); + if (!state.saveMessage) { + return; + } + const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_DESCRIPTION, {}); + const newMessage = createMessage({ meta, hash, data: notificationData, state }); + state.messages.push(newMessage); + }, + sideEffect({ contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + if (module_default("okTurtles.data/get", "READY_TO_JOIN_CHATROOM")) { + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + } + }, + "gi.contracts/chatroom/leave": { + validate: objectOf({ + username: optional(string), + member: string + }), + process({ data, meta, hash }, { state }) { + const { member } = data; + const isKicked = data.username && member !== data.username; + if (!state.saveMessage && !state.users[member]) { + throw new Error(`Can not leave the chatroom which ${member} are not part of`); + } + default2.delete(state.users, member); + if (!state.saveMessage) { + return; + } + const notificationType = !isKicked ? MESSAGE_NOTIFICATIONS.LEAVE_MEMBER : MESSAGE_NOTIFICATIONS.KICK_MEMBER; + const notificationData = createNotificationData(notificationType, isKicked ? { username: member } : {}); + const newMessage = createMessage({ + meta: isKicked ? meta : { ...meta, username: member }, + hash, + data: notificationData, + state + }); + state.messages.push(newMessage); + }, + sideEffect({ data, hash, contractID, meta }, { state }) { + const rootState = module_default("state/vuex/state"); + if (data.member === rootState.loggedIn.username) { + if (module_default("okTurtles.data/get", "READY_TO_JOIN_CHATROOM")) { + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + if (module_default("okTurtles.data/get", "JOINING_CHATROOM_ID")) { + return; + } + leaveChatRoom({ contractID }); + } + emitMessageEvent({ contractID, hash }); + } + }, + "gi.contracts/chatroom/delete": { + validate: (data, { state, meta }) => { + if (state.attributes.creator !== meta.username) { + throw new TypeError(L("Only the channel creator can delete channel.")); + } + }, + process({ data, meta }, { state, rootState }) { + default2.set(state.attributes, "deletedDate", meta.createdDate); + for (const username in state.users) { + default2.delete(state.users, username); + } + }, + sideEffect({ meta, contractID }, { state }) { + if (module_default("okTurtles.data/get", "JOINING_CHATROOM_ID")) { + return; + } + leaveChatRoom({ contractID }); + } + }, + "gi.contracts/chatroom/addMessage": { + validate: messageType, + process({ data, meta, hash }, { state }) { + if (!state.saveMessage) { + return; + } + const pendingMsg = state.messages.find((msg) => msg.id === hash && msg.pending); + if (pendingMsg) { + delete pendingMsg.pending; + } else { + state.messages.push(createMessage({ meta, data, hash, state })); + } + }, + sideEffect({ contractID, hash, meta, data }, { state, getters }) { + emitMessageEvent({ contractID, hash }); + const rootState = module_default("state/vuex/state"); + const me = rootState.loggedIn.username; + if (me === meta.username) { + return; + } + const newMessage = createMessage({ meta, data, hash, state }); + const mentions = makeMentionFromUsername(me); + if (data.type === MESSAGE_TYPES.TEXT && (newMessage.text.includes(mentions.me) || newMessage.text.includes(mentions.all))) { + addMention({ + contractID, + messageId: newMessage.id, + datetime: newMessage.datetime, + text: newMessage.text, + username: meta.username, + chatRoomName: getters.chatRoomAttributes.name + }); + } + if (module_default("okTurtles.data/get", "READY_TO_JOIN_CHATROOM")) { + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + } + }, + "gi.contracts/chatroom/editMessage": { + validate: (data, { state, meta }) => { + objectOf({ + id: string, + createdDate: string, + text: string + })(data); + }, + process({ data, meta }, { state }) { + if (!state.saveMessage) { + return; + } + const msgIndex = findMessageIdx(data.id, state.messages); + if (msgIndex >= 0 && meta.username === state.messages[msgIndex].from) { + state.messages[msgIndex].text = data.text; + state.messages[msgIndex].updatedDate = meta.createdDate; + if (state.saveMessage && state.messages[msgIndex].pending) { + delete state.messages[msgIndex].pending; + } + } + }, + sideEffect({ contractID, hash, meta, data }, { getters }) { + emitMessageEvent({ contractID, hash }); + const rootState = module_default("state/vuex/state"); + const me = rootState.loggedIn.username; + if (me === meta.username) { + return; + } + const isAlreadyAdded = rootState.chatRoomUnread[contractID].mentions.find((m) => m.messageId === data.id); + const mentions = makeMentionFromUsername(me); + const isIncludeMention = data.text.includes(mentions.me) || data.text.includes(mentions.all); + if (!isAlreadyAdded && isIncludeMention) { + addMention({ + contractID, + messageId: data.id, + datetime: data.createdDate, + text: data.text, + username: meta.username, + chatRoomName: getters.chatRoomAttributes.name + }); + } else if (isAlreadyAdded && !isIncludeMention) { + deleteMention({ contractID, messageId: data.id }); + } + } + }, + "gi.contracts/chatroom/deleteMessage": { + validate: objectOf({ + id: string + }), + process({ data, meta }, { state }) { + if (!state.saveMessage) { + return; + } + const msgIndex = findMessageIdx(data.id, state.messages); + if (msgIndex >= 0) { + state.messages.splice(msgIndex, 1); + } + for (const message of state.messages) { + if (message.replyingMessage?.id === data.id) { + message.replyingMessage.id = null; + message.replyingMessage.text = "Original message was removed."; + } + } + }, + sideEffect({ data, contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + const rootState = module_default("state/vuex/state"); + const me = rootState.loggedIn.username; + if (rootState.chatRoomScrollPosition[contractID] === data.id) { + module_default("state/vuex/commit", "setChatRoomScrollPosition", { + chatRoomId: contractID, + messageId: null + }); + } + if (rootState.chatRoomUnread[contractID].since.messageId === data.id) { + module_default("state/vuex/commit", "deleteChatRoomUnreadSince", { + chatRoomId: contractID, + deletedDate: meta.createdDate + }); + } + if (me === meta.username) { + return; + } + if (rootState.chatRoomUnread[contractID].mentions.find((m) => m.messageId === data.id)) { + deleteMention({ contractID, messageId: data.id }); + } + emitMessageEvent({ contractID, hash }); + } + }, + "gi.contracts/chatroom/makeEmotion": { + validate: objectOf({ + id: string, + emoticon: string + }), + process({ data, meta, contractID }, { state }) { + if (!state.saveMessage) { + return; + } + const { id, emoticon } = data; + const msgIndex = findMessageIdx(id, state.messages); + if (msgIndex >= 0) { + let emoticons = cloneDeep(state.messages[msgIndex].emoticons || {}); + if (emoticons[emoticon]) { + const alreadyAdded = emoticons[emoticon].indexOf(meta.username); + if (alreadyAdded >= 0) { + emoticons[emoticon].splice(alreadyAdded, 1); + if (!emoticons[emoticon].length) { + delete emoticons[emoticon]; + if (!Object.keys(emoticons).length) { + emoticons = null; + } + } + } else { + emoticons[emoticon].push(meta.username); + } + } else { + emoticons[emoticon] = [meta.username]; + } + if (emoticons) { + default2.set(state.messages[msgIndex], "emoticons", emoticons); + } else { + default2.delete(state.messages[msgIndex], "emoticons"); + } + } + }, + sideEffect({ contractID, hash }) { + emitMessageEvent({ contractID, hash }); + } + } + } +}); +//# sourceMappingURL=chatroom.js.map diff --git a/test/contracts/chatroom.js.map b/test/contracts/chatroom.js.map new file mode 100644 index 0000000000..2aba20bd13 --- /dev/null +++ b/test/contracts/chatroom.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/shared/time.js", "../../frontend/views/utils/misc.js", "../../frontend/model/contracts/shared/functions.js", "../../frontend/model/contracts/shared/nativeNotification.js", "../../frontend/model/contracts/chatroom.js"], + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nexport function logExceptNavigationDuplicated (err ) {\n err.name !== 'NavigationDuplicated' && console.error(err)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { INVITE_STATUS, MESSAGE_TYPES } from './constants.js'\nimport { DAYS_MILLIS } from './time.js'\nimport { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js'\n\n// !!!!!!!!!!!!!!!\n// !! IMPORTANT !!\n// !!!!!!!!!!!!!!!\n//\n// DO NOT CHANGE THE LOGIC TO ANY OF THESE FUNCTIONS!\n// INSTEAD, CREATE NEW FUNCTIONS WITH DIFFERENT NAMES\n// AND USE THOSE INSTEAD!\n//\n// THIS IS A CONSEQUENCE OF SHARING THIS CODE WITH THE REST OF THE APP.\n// IF YOU DO NOT NEED TO SHARE CODE WITH THE REST OF THE APP (AND CAN\n// KEEP IT WITHIN THE CONTRACT ONLY), THEN YOU DON'T NEED TO WORRY ABOUT\n// THIS, AND SHOULD INCLUDE THOSE FUNCTIONS (WITHOUT EXPORTING THEM),\n// DIRECTLY IN YOUR CONTRACT DEFINITION FILE. THEN YOU CAN MODIFY\n// THEM AS MUCH AS YOU LIKE (and generate new contract versions out of them).\n\n// group.js related\n\nexport function createInvite ({ quantity = 1, creator, expires, invitee } \n \n ) \n \n \n \n \n \n \n \n {\n return {\n inviteSecret: `${parseInt(Math.random() * 10000)}`, // TODO: this\n quantity,\n creator,\n invitee,\n status: INVITE_STATUS.VALID,\n responses: {}, // { bob: true } list of usernames that accepted the invite.\n expires: Date.now() + DAYS_MILLIS * expires\n }\n}\n\n// chatroom.js related\n\nexport function createMessage ({ meta, data, hash, state } \n \n ) {\n const { type, text, replyingMessage } = data\n const { createdDate } = meta\n\n let newMessage = {\n type,\n datetime: new Date(createdDate).toISOString(),\n id: hash,\n from: meta.username\n }\n\n if (type === MESSAGE_TYPES.TEXT) {\n newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }\n } else if (type === MESSAGE_TYPES.POLL) {\n // TODO: Poll message creation\n } else if (type === MESSAGE_TYPES.NOTIFICATION) {\n const params = {\n channelName: state?.attributes.name,\n channelDescription: state?.attributes.description,\n ...data.notification\n }\n delete params.type\n newMessage = {\n ...newMessage,\n notification: { type: data.notification.type, params }\n }\n } else if (type === MESSAGE_TYPES.INTERACTIVE) {\n // TODO: Interactive message creation for proposals\n }\n return newMessage\n}\n\nexport async function leaveChatRoom ({ contractID } \n \n ) {\n const rootState = sbp('state/vuex/state')\n const rootGetters = sbp('state/vuex/getters')\n if (contractID === rootGetters.currentChatRoomId) {\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {\n groupId: rootState.currentGroupId\n })\n const curRouteName = sbp('controller/router').history.current.name\n if (curRouteName === 'GroupChat' || curRouteName === 'GroupChatConversation') {\n await sbp('controller/router')\n .push({ name: 'GroupChatConversation', params: { chatRoomId: rootGetters.currentChatRoomId } })\n .catch(logExceptNavigationDuplicated)\n }\n }\n\n sbp('state/vuex/commit', 'deleteChatRoomUnread', { chatRoomId: contractID })\n sbp('state/vuex/commit', 'deleteChatRoomScrollPosition', { chatRoomId: contractID })\n\n // NOTE: make sure *not* to await on this, since that can cause\n // a potential deadlock. See same warning in sideEffect for\n // 'gi.contracts/group/removeMember'\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e)\n })\n}\n\nexport function findMessageIdx (id , messages ) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].id === id) {\n return i\n }\n }\n return -1\n}\n\nexport function makeMentionFromUsername (username ) \n \n {\n return {\n me: `@${username}`,\n all: '@all'\n }\n}\n", "'use strict'\nimport sbp from '@sbp/sbp'\n\n// NOTE: since these functions don't modify contract state, it should\n// be safe to modify them without worrying about version conflicts.\n\nexport async function requestNotificationPermission (force = false) {\n if (typeof Notification === 'undefined') {\n return null\n }\n if (force || Notification.permission === 'default') {\n try {\n sbp('state/vuex/commit', 'setNotificationEnabled', await Notification.requestPermission() === 'granted')\n } catch (e) {\n console.error('requestNotificationPermission:', e.message)\n return null\n }\n }\n return Notification.permission\n}\n\nexport function makeNotification ({ title, body, icon, path } \n \n ) {\n const notificationEnabled = sbp('state/vuex/state').notificationEnabled\n if (typeof Notification === 'undefined' || Notification.permission !== 'granted' || !notificationEnabled) {\n return\n }\n\n const notification = new Notification(title, { body, icon })\n if (path) {\n notification.onclick = function (event) {\n event.preventDefault()\n sbp('controller/router').push({ path }).catch(console.warn)\n }\n }\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, L } from '@common/common.js'\nimport { merge, cloneDeep } from './shared/giLodash.js'\nimport {\n CHATROOM_NAME_LIMITS_IN_CHARS,\n CHATROOM_DESCRIPTION_LIMITS_IN_CHARS,\n CHATROOM_ACTIONS_PER_PAGE,\n CHATROOM_MESSAGES_PER_PAGE,\n MESSAGE_TYPES,\n MESSAGE_NOTIFICATIONS,\n CHATROOM_MESSAGE_ACTION,\n MESSAGE_RECEIVE\n} from './shared/constants.js'\nimport { chatRoomAttributesType, messageType } from './shared/types.js'\nimport { createMessage, leaveChatRoom, findMessageIdx, makeMentionFromUsername } from './shared/functions.js'\nimport { makeNotification } from './shared/nativeNotification.js'\nimport { objectOf, string, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction createNotificationData (\n notificationType ,\n moreParams = {}\n) {\n return {\n type: MESSAGE_TYPES.NOTIFICATION,\n notification: {\n type: notificationType,\n ...moreParams\n }\n }\n}\n\nfunction emitMessageEvent ({ contractID, hash } \n \n \n ) {\n sbp('okTurtles.events/emit', `${CHATROOM_MESSAGE_ACTION}-${contractID}`, { hash })\n}\n\nfunction addMention ({ contractID, messageId, datetime, text, username, chatRoomName } \n \n \n \n \n \n \n ) {\n /**\n * If 'READY_TO_JOIN_CHATROOM' is false, it means not syncing chatroom\n */\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n return\n }\n\n sbp('state/vuex/commit', 'addChatRoomUnreadMention', {\n chatRoomId: contractID,\n messageId,\n createdDate: datetime\n })\n\n const rootGetters = sbp('state/vuex/getters')\n const groupID = rootGetters.groupIdFromChatRoomId(contractID)\n const path = `/group-chat/${contractID}`\n\n makeNotification({\n title: `# ${chatRoomName}`,\n body: text,\n icon: rootGetters.globalProfile2(groupID, username).picture,\n path\n })\n\n sbp('okTurtles.events/emit', MESSAGE_RECEIVE)\n}\n\nfunction deleteMention ({ contractID, messageId } \n \n ) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadMention', { chatRoomId: contractID, messageId })\n}\n\nfunction updateUnreadPosition ({ contractID, hash, createdDate } \n \n ) {\n sbp('state/vuex/commit', 'setChatRoomUnreadSince', {\n chatRoomId: contractID,\n messageId: hash,\n createdDate\n })\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/chatroom',\n metadata: {\n validate: objectOf({\n createdDate: string, // action created date\n username: string, // action creator\n identityContractID: string // action creator identityContractID\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n getters: {\n currentChatRoomState (state) {\n return state\n },\n chatRoomSettings (state, getters) {\n return getters.currentChatRoomState.settings || {}\n },\n chatRoomAttributes (state, getters) {\n return getters.currentChatRoomState.attributes || {}\n },\n chatRoomUsers (state, getters) {\n return getters.currentChatRoomState.users || {}\n },\n chatRoomLatestMessages (state, getters) {\n return getters.currentChatRoomState.messages || []\n }\n },\n actions: {\n // This is the constructor of Chat contract\n 'gi.contracts/chatroom': {\n validate: objectOf({\n attributes: chatRoomAttributesType\n }),\n process ({ meta, data }, { state }) {\n const initialState = merge({\n settings: {\n actionsPerPage: CHATROOM_ACTIONS_PER_PAGE,\n messagesPerPage: CHATROOM_MESSAGES_PER_PAGE,\n maxNameLength: CHATROOM_NAME_LIMITS_IN_CHARS,\n maxDescriptionLength: CHATROOM_DESCRIPTION_LIMITS_IN_CHARS\n },\n attributes: {\n creator: meta.username,\n deletedDate: null,\n archivedDate: null\n },\n users: {},\n messages: []\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/chatroom/join': {\n validate: objectOf({\n username: string // username of joining member\n }),\n process ({ data, meta, hash }, { state }) {\n const { username } = data\n if (!state.saveMessage && state.users[username]) {\n // this can happen when we're logging in on another machine, and also in other circumstances\n console.warn('Can not join the chatroom which you are already part of')\n return\n }\n\n Vue.set(state.users, username, { joinedDate: meta.createdDate })\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = username === meta.username ? MESSAGE_NOTIFICATIONS.JOIN_MEMBER : MESSAGE_NOTIFICATIONS.ADD_MEMBER\n const notificationData = createNotificationData(\n notificationType,\n notificationType === MESSAGE_NOTIFICATIONS.ADD_MEMBER ? { username } : {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM') || // Join by himself or Login in another device\n sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID') === contractID) { // Be added by another\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/rename': {\n validate: objectOf({\n name: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'name', data.name)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_NAME, {})\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/changeDescription': {\n validate: objectOf({\n description: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'description', data.description)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(\n MESSAGE_NOTIFICATIONS.UPDATE_DESCRIPTION, {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/leave': {\n validate: objectOf({\n username: optional(string), // coming from the gi.contracts/group/leaveChatRoom\n member: string // username to be removed\n }),\n process ({ data, meta, hash }, { state }) {\n const { member } = data\n const isKicked = data.username && member !== data.username\n if (!state.saveMessage && !state.users[member]) {\n throw new Error(`Can not leave the chatroom which ${member} are not part of`)\n }\n Vue.delete(state.users, member)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = !isKicked ? MESSAGE_NOTIFICATIONS.LEAVE_MEMBER : MESSAGE_NOTIFICATIONS.KICK_MEMBER\n const notificationData = createNotificationData(notificationType, isKicked ? { username: member } : {})\n const newMessage = createMessage({\n meta: isKicked ? meta : { ...meta, username: member },\n hash,\n data: notificationData,\n state\n })\n state.messages.push(newMessage)\n },\n sideEffect ({ data, hash, contractID, meta }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (data.member === rootState.loggedIn.username) {\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/delete': {\n validate: (data, { state, meta }) => {\n if (state.attributes.creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state, rootState }) {\n Vue.set(state.attributes, 'deletedDate', meta.createdDate)\n for (const username in state.users) {\n Vue.delete(state.users, username)\n }\n },\n sideEffect ({ meta, contractID }, { state }) {\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n },\n 'gi.contracts/chatroom/addMessage': {\n validate: messageType,\n process ({ data, meta, hash }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const pendingMsg = state.messages.find(msg => msg.id === hash && msg.pending)\n if (pendingMsg) {\n delete pendingMsg.pending\n } else {\n state.messages.push(createMessage({ meta, data, hash, state }))\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { state, getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const newMessage = createMessage({ meta, data, hash, state })\n const mentions = makeMentionFromUsername(me)\n if (data.type === MESSAGE_TYPES.TEXT &&\n (newMessage.text.includes(mentions.me) || newMessage.text.includes(mentions.all))) {\n addMention({\n contractID,\n messageId: newMessage.id,\n datetime: newMessage.datetime,\n text: newMessage.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n }\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/editMessage': {\n validate: (data, { state, meta }) => {\n objectOf({\n id: string,\n createdDate: string,\n text: string\n })(data)\n // TODO: Actually NOT SURE it's needed to check if the meta.username === message.from\n // there is no messagess in vuex state\n // to check if the meta.username is creator seems like too heavy\n },\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0 && meta.username === state.messages[msgIndex].from) {\n state.messages[msgIndex].text = data.text\n state.messages[msgIndex].updatedDate = meta.createdDate\n if (state.saveMessage && state.messages[msgIndex].pending) {\n delete state.messages[msgIndex].pending\n }\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const isAlreadyAdded = rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)\n const mentions = makeMentionFromUsername(me)\n const isIncludeMention = data.text.includes(mentions.me) || data.text.includes(mentions.all)\n if (!isAlreadyAdded && isIncludeMention) {\n addMention({\n contractID,\n messageId: data.id,\n /*\n * the following datetime is the time when the message(which made mention) is created\n * the reason why it is it instead of datetime when the mention created is because\n * it is compared to the datetime of other messages when user scrolls\n * to decide if it should be removed from the list of mentions or not\n */\n datetime: data.createdDate,\n text: data.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n } else if (isAlreadyAdded && !isIncludeMention) {\n deleteMention({ contractID, messageId: data.id })\n }\n }\n },\n 'gi.contracts/chatroom/deleteMessage': {\n validate: objectOf({\n id: string\n }),\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0) {\n state.messages.splice(msgIndex, 1)\n }\n // filter replied messages and check if the current message is original\n for (const message of state.messages) {\n if (message.replyingMessage?.id === data.id) {\n message.replyingMessage.id = null\n message.replyingMessage.text = 'Original message was removed.'\n }\n }\n },\n sideEffect ({ data, contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (rootState.chatRoomScrollPosition[contractID] === data.id) {\n sbp('state/vuex/commit', 'setChatRoomScrollPosition', {\n chatRoomId: contractID, messageId: null\n })\n }\n\n if (rootState.chatRoomUnread[contractID].since.messageId === data.id) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadSince', {\n chatRoomId: contractID,\n deletedDate: meta.createdDate\n })\n }\n\n if (me === meta.username) {\n return\n }\n if (rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)) {\n deleteMention({ contractID, messageId: data.id })\n }\n\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/makeEmotion': {\n validate: objectOf({\n id: string,\n emoticon: string\n }),\n process ({ data, meta, contractID }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const { id, emoticon } = data\n const msgIndex = findMessageIdx(id, state.messages)\n if (msgIndex >= 0) {\n let emoticons = cloneDeep(state.messages[msgIndex].emoticons || {})\n if (emoticons[emoticon]) {\n const alreadyAdded = emoticons[emoticon].indexOf(meta.username)\n if (alreadyAdded >= 0) {\n emoticons[emoticon].splice(alreadyAdded, 1)\n if (!emoticons[emoticon].length) {\n delete emoticons[emoticon]\n if (!Object.keys(emoticons).length) {\n emoticons = null\n }\n }\n } else {\n emoticons[emoticon].push(meta.username)\n }\n } else {\n emoticons[emoticon] = [meta.username]\n }\n if (emoticons) {\n Vue.set(state.messages[msgIndex], 'emoticons', emoticons)\n } else {\n Vue.delete(state.messages[msgIndex], 'emoticons')\n }\n }\n },\n sideEffect ({ contractID, hash }) {\n emitMessageEvent({ contractID, hash })\n }\n }\n }\n})\n"], + "mappings": ";;;AAKA,IAAM,YAAY,CAAC;AACnB,IAAM,UAAU,CAAC;AACjB,IAAM,gBAAgB,CAAC;AACvB,IAAM,gBAAgB,CAAC;AACvB,IAAM,kBAAkB,CAAC;AACzB,IAAM,kBAAkB,CAAC;AAEzB,IAAM,eAAe;AAErB,aAAc,aAAa,MAAM;AAC/B,QAAM,SAAS,mBAAmB,QAAQ;AAC1C,MAAI,CAAC,UAAU,WAAW;AACxB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AAGA,aAAW,WAAW,CAAC,gBAAgB,WAAW,cAAc,SAAS,aAAa,GAAG;AACvF,QAAI,SAAS;AACX,iBAAW,UAAU,SAAS;AAC5B,YAAI,OAAO,QAAQ,UAAU,IAAI,MAAM;AAAO;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACA,SAAO,UAAU,UAAU,KAAK,QAAQ,QAAQ,OAAO,GAAG,IAAI;AAChE;AAEO,4BAA6B,UAAU;AAC5C,QAAM,eAAe,aAAa,KAAK,QAAQ;AAC/C,MAAI,iBAAiB,MAAM;AACzB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AACA,SAAO,aAAa;AACtB;AAEA,IAAM,qBAAqB;AAAA,EACzB,0BAA0B,SAAU,MAAM;AACxC,UAAM,aAAa,CAAC;AACpB,eAAW,YAAY,MAAM;AAC3B,YAAM,SAAS,mBAAmB,QAAQ;AAC1C,UAAI,UAAU,WAAW;AACvB,QAAC,SAAQ,QAAQ,QAAQ,KAAK,6DAA6D,WAAW;AAAA,MACxG,WAAW,OAAO,KAAK,cAAc,YAAY;AAC/C,YAAI,gBAAgB,WAAW;AAE7B,UAAC,SAAQ,QAAQ,QAAQ,KAAK,6CAA6C,gDAAgD;AAAA,QAC7H;AACA,cAAM,KAAK,UAAU,YAAY,KAAK;AACtC,mBAAW,KAAK,QAAQ;AAExB,YAAI,CAAC,QAAQ,SAAS;AACpB,kBAAQ,UAAU,EAAE,OAAO,CAAC,EAAE;AAAA,QAChC;AAEA,YAAI,aAAa,GAAG,gBAAgB;AAClC,aAAG,KAAK,QAAQ,QAAQ,KAAK;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,4BAA4B,SAAU,MAAM;AAC1C,eAAW,YAAY,MAAM;AAC3B,UAAI,CAAC,gBAAgB,WAAW;AAC9B,cAAM,IAAI,MAAM,0CAA0C,UAAU;AAAA,MACtE;AACA,aAAO,UAAU;AAAA,IACnB;AAAA,EACF;AAAA,EACA,2BAA2B,SAAU,MAAM;AACzC,QAAI,4BAA4B,OAAO,KAAK,IAAI,CAAC;AACjD,WAAO,IAAI,0BAA0B,IAAI;AAAA,EAC3C;AAAA,EACA,wBAAwB,SAAU,MAAM;AACtC,eAAW,YAAY,MAAM;AAC3B,UAAI,UAAU,WAAW;AACvB,cAAM,IAAI,MAAM,mDAAmD;AAAA,MACrE;AACA,sBAAgB,YAAY;AAAA,IAC9B;AAAA,EACF;AAAA,EACA,sBAAsB,SAAU,MAAM;AACpC,eAAW,YAAY,MAAM;AAC3B,aAAO,gBAAgB;AAAA,IACzB;AAAA,EACF;AAAA,EACA,oBAAoB,SAAU,KAAK;AACjC,WAAO,UAAU;AAAA,EACnB;AAAA,EACA,0BAA0B,SAAU,QAAQ;AAC1C,kBAAc,KAAK,MAAM;AAAA,EAC3B;AAAA,EACA,0BAA0B,SAAU,QAAQ,QAAQ;AAClD,QAAI,CAAC,cAAc;AAAS,oBAAc,UAAU,CAAC;AACrD,kBAAc,QAAQ,KAAK,MAAM;AAAA,EACnC;AAAA,EACA,4BAA4B,SAAU,UAAU,QAAQ;AACtD,QAAI,CAAC,gBAAgB;AAAW,sBAAgB,YAAY,CAAC;AAC7D,oBAAgB,UAAU,KAAK,MAAM;AAAA,EACvC;AACF;AAEA,mBAAmB,0BAA0B,kBAAkB;AAE/D,IAAO,iBAAQ;;;ACpFf;;;ACNA;AACA;;;ACwBO,mBAAoB,KAAK;AAC9B,SAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;AACvC;AAEA,2BAA4B,KAAK;AAC/B,QAAM,gBAAgB,OAAO,OAAO,QAAQ;AAE5C,SAAO,iBAAiB,OAAO,UAAU,SAAS,KAAK,GAAG,MAAM,qBAAqB,OAAO,UAAU,SAAS,KAAK,GAAG,MAAM;AAC/H;AAEO,eAAgB,KAAK,KAAK;AAC/B,aAAW,OAAO,KAAK;AACrB,UAAM,QAAQ,kBAAkB,IAAI,IAAI,IAAI,UAAU,IAAI,IAAI,IAAI;AAClE,QAAI,SAAS,kBAAkB,IAAI,IAAI,GAAG;AACxC,YAAM,IAAI,MAAM,KAAK;AACrB;AAAA,IACF;AACA,QAAI,OAAO,SAAS,IAAI;AAAA,EAC1B;AACA,SAAO;AACT;;;ADxCO,IAAM,gBAAgB;AAAA,EAC3B,cAAc,CAAC,OAAO;AAAA,EACtB,cAAc,CAAC,KAAK,MAAM,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EAEtF,qBAAqB;AACvB;AAEA,IAAM,YAAY,CAAC,IAAI,YAAY;AACjC,MAAI,QAAQ,aAAa,QAAQ,OAAO;AACtC,QAAI,SAAS;AACb,QAAI,QAAQ,QAAQ,KAAK;AACvB,eAAS,UAAU,MAAM;AACzB,aAAO,aAAa,KAAK,QAAQ,QAAQ;AACzC,aAAO,aAAa,KAAK,GAAG;AAAA,IAC9B;AACA,OAAG,cAAc;AACjB,OAAG,YAAY,UAAU,SAAS,QAAQ,OAAO,MAAM,CAAC;AAAA,EAC1D;AACF;AAWA,IAAI,UAAU,aAAa;AAAA,EACzB,MAAM;AAAA,EACN,QAAQ;AACV,CAAC;;;AElDD;AACA;;;ACNA,IAAM,QAAQ;AAEC,kBAAmB,YAAmB,MAAqB;AACxE,QAAM,WAAW,KAAK;AAGtB,QAAM,oBACH,OAAO,aAAa,YAAY,aAAa,OAC1C,WACA;AAGN,SAAO,QAAO,QAAQ,OAAO,oBAAqB,OAAO,SAAS,OAAO;AAEvE,QAAI,QAAO,QAAQ,OAAO,OAAO,QAAO,QAAQ,MAAM,YAAY,KAAK;AACrE,aAAO;AAAA,IACT;AAEA,UAAM,mBAGJ,OAAO,UAAU,eAAe,KAAK,mBAAmB,OAAO,IAC3D,kBAAkB,WAClB;AAGN,QAAI,qBAAqB,QAAQ,qBAAqB,QAAW;AAC/D,aAAO;AAAA,IACT;AACA,WAAO,OAAO,gBAAgB;AAAA,EAChC,CAAC;AACH;;;ADtBA,KAAI,UAAU,IAAI;AAClB,KAAI,UAAU,QAAQ;AAEtB,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,0BAAgD,CAAC;AAOvD,IAAM,kBAAkB;AAAA,EACtB,GAAG;AAAA,EACH,cAAc,CAAC,SAAS,QAAQ,OAAO,QAAQ;AAAA,EAC/C,cAAc,CAAC,KAAK,KAAK,MAAM,UAAU,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EACrG,qBAAqB;AACvB;AAEA,IAAI,kBAAkB;AACtB,IAAI,sBAAsB,gBAAgB,MAAM,GAAG,EAAE;AACrD,IAAI,0BAA0B;AAY9B,eAAI,0BAA0B;AAAA,EAC5B,qBAAqB,oBAAqB,UAAiC;AAEzE,UAAM,CAAC,gBAAgB,SAAS,YAAY,EAAE,MAAM,GAAG;AAGvD,QAAI,SAAS,YAAY,MAAM,gBAAgB,YAAY;AAAG;AAI9D,QAAI,iBAAiB;AAAqB;AAG1C,QAAI,iBAAiB,qBAAqB;AACxC,wBAAkB;AAClB,4BAAsB;AACtB,gCAA0B;AAC1B;AAAA,IACF;AACA,QAAI;AACF,gCAA2B,MAAM,eAAI,4BAA4B,QAAQ,KAAM;AAG/E,wBAAkB;AAClB,4BAAsB;AAAA,IACxB,SAAS,OAAP;AACA,cAAQ,MAAM,KAAK;AAAA,IACrB;AAAA,EACF;AACF,CAAC;AA0CM,kBAAmB,MAAiC;AACzD,QAAM,IAAI;AAAA,IACR,OAAO;AAAA,EACT;AACA,aAAW,OAAO,MAAM;AACtB,MAAE,GAAG,UAAU,IAAI;AACnB,MAAE,IAAI,SAAS,KAAK;AAAA,EACtB;AACA,SAAO;AACT;AAEe,WACb,KACA,MACQ;AACR,SAAO,SAAS,wBAAwB,QAAQ,KAAK,IAAI,EAEtD,QAAQ,iBAAiB,QAAQ;AACtC;AAgBA,kBAAmB,aAAa;AAC9B,SAAO,WAAU,SAAS,aAAa,eAAe;AACxD;AAEA,KAAI,UAAU,QAAQ;AAAA,EACpB,YAAY;AAAA,EACZ,OAAO;AAAA,IACL,MAAM,CAAC,QAAQ,KAAK;AAAA,IACpB,KAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA,SAAS;AAAA,EACX;AAAA,EACA,QAAQ,SAAU,GAAG,SAAS;AAC5B,UAAM,OAAO,QAAQ,SAAS,GAAG;AACjC,UAAM,cAAc,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,CAAC;AACpD,QAAI,CAAC,aAAa;AAChB,cAAQ,KAAK,yDAAyD,IAAI;AAC1E,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,IAAI;AAAA,IAChD;AAEA,QAAI,QAAQ,MAAM,QAAQ,OAAO,QAAQ,KAAK,MAAM,WAAW,UAAU;AACvE,cAAQ,KAAK,MAAM,MAAM;AAAA,IAC3B;AACA,QAAI,QAAQ,MAAM,SAAS;AACzB,YAAM,SAAS,KAAI,QAAQ,WAAW,SAAS,WAAW,IAAI,SAAS;AAEvE,aAAO,OAAO,OAAO,KAAK;AAAA,QACxB,IAAI,CAAC,QAAQ,SAAS;AACpB,cAAI,QAAQ,QAAQ;AAClB,mBAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,GAAG,IAAI;AAAA,UACnD,OAAO;AACL,mBAAO,EAAE,KAAK,GAAG,IAAI;AAAA,UACvB;AAAA,QACF;AAAA,QACA,IAAI,OAAK;AAAA,MACX,CAAC;AAAA,IACH,OAAO;AACL,UAAI,CAAC,QAAQ,KAAK;AAAU,gBAAQ,KAAK,WAAW,CAAC;AACrD,cAAQ,KAAK,SAAS,YAAY,SAAS,WAAW;AACtD,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,IAAI;AAAA,IAC1C;AAAA,EACF;AACF,CAAC;;;AEvJM,IAAM,gCAAgC;AACtC,IAAM,uCAAuC;AAC7C,IAAM,4BAA4B;AAClC,IAAM,6BAA6B;AAGnC,IAAM,0BAA0B;AAEhC,IAAM,kBAAkB;AAGxB,IAAM,iBAAiB;AAAA,EAC5B,YAAY;AAAA,EACZ,OAAO;AACT;AAEO,IAAM,yBAAyB;AAAA,EACpC,OAAO;AAAA,EACP,SAAS;AAAA,EACT,QAAQ;AACV;AAEO,IAAM,gBAAgB;AAAA,EAC3B,MAAM;AAAA,EACN,MAAM;AAAA,EACN,aAAa;AAAA,EACb,cAAc;AAChB;AAOO,IAAM,wBAAwB;AAAA,EACnC,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,MAAM;AACR;AAWO,IAAM,oBAAoB;AAC1B,IAAM,uBAAuB;;;AC5C7B,IAAM,cAAc,OAAO,SAAS;AACpC,IAAM,UAAU,OAAK,MAAM;AAC3B,IAAM,QAAQ,OAAK,MAAM;AACzB,IAAM,UAAU,OAAK,OAAO,MAAM;AAClC,IAAM,YAAY,OAAK,OAAO,MAAM;AACpC,IAAM,WAAW,OAAK,OAAO,MAAM;AACnC,IAAM,WAAW,OAAK,OAAO,MAAM;AACnC,IAAM,WAAW,OAAK,CAAC,MAAM,CAAC,KAAK,OAAO,MAAM;AAChD,IAAM,aAAa,OAAK,OAAO,MAAM;AAcrC,IAAM,UAAU,CAAC,QAAQ,aAAa;AAC3C,MAAI,WAAW,OAAO,IAAI;AAAG,WAAO,OAAO,KAAK,QAAQ;AACxD,SAAO,OAAO,QAAQ;AACxB;AAGO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YACE,SACA,cACA,WACA,OACA,WAAmB,IACnB,YAAqB,IACrB;AACA,UAAM,aAAa,WACjB,YAAY,0BAA0B,YAAY;AACpD,UAAM,UAAU;AAChB,SAAK,eAAe;AACpB,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,YAAY,aAAa;AAC9B,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,UAAU,GAAG;AAAA,EAAe,KAAK,aAAa;AACnD,SAAK,OAAO,KAAK,YAAY;AAC7B,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,kBAAkB;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,gBAAyB;AACvB,UAAM,YAAY,KAAK,MAAM,MAAM,gCAAgC,KAAK,CAAC;AACzE,WAAO,UAAU,KAAK,cAAY,SAAS,QAAQ,qBAAqB,MAAM,EAAE,KAAK;AAAA,EACvF;AAAA,EAEA,eAAwB;AACtB,WAAO;AAAA,eACI,KAAK;AAAA,eACL,KAAK;AAAA,eACL,KAAK,aAAa,QAAQ,OAAO,EAAE;AAAA,eACnC,KAAK;AAAA,eACL,KAAK;AAAA;AAAA,EAElB;AACF;AAKA,IAAM,iBAAoB,CACxB,QACA,OACA,OACA,SACA,cACA,cACuB;AACvB,SAAO,IAAI,mBACT,SACA,gBAAgB,QAAQ,MAAM,GAC9B,aAAa,OAAO,OACpB,KAAK,UAAU,KAAK,GACpB,OAAO,MACP,KACF;AACF;AAEO,IAAM,UACR,CAAC,QAA0B,SAAkB,YAAmC;AACjF,iBAAgB,OAAO;AACrB,QAAI,QAAQ,KAAK;AAAG,aAAO,CAAC,OAAO,KAAK,CAAC;AACzC,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,UAAI,QAAQ;AACZ,aAAO,MAAM,IAAI,OAAK,OAAO,GAAG,GAAG,UAAU,UAAU,CAAC;AAAA,IAC1D;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,SAAS,QAAQ,MAAM;AAC1C,SAAO;AACT;AAEK,IAAM,YACM,CAAC,cAAmC;AACnD,mBAAkB,OAAO,SAAS,IAAI;AACpC,QAAI,QAAQ,KAAK,KAAM,UAAU;AAAY,aAAO;AACpD,UAAM,eAAe,SAAS,OAAO,MAAM;AAAA,EAC7C;AACA,UAAQ,OAAO,MAAM;AACnB,QAAI,UAAU,SAAS;AAAG,aAAO,GAAG,YAAY,SAAS;AAAA;AACpD,aAAO,IAAI;AAAA,EAClB;AACA,SAAO;AACT;AAEK,IAAM,QAAc,CACzB,WACA,WAC8B;AAC9B,kBAAgB,OAAO;AACrB,QAAI,QAAQ,KAAK;AAAG,aAAO,CAAC;AAC5B,UAAM,IAAI,OAAO,KAAK;AACtB,UAAM,UAAU,CAAC,KAAK,QACpB,OAAO,OACL,KACA;AAAA,MAEE,CAAC,UAAU,KAAK,QAAQ,IAAI,OAAO,EAAE,MAAM,OAAO,KAAK;AAAA,IACzD,CACF;AACF,WAAO,OAAO,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC,CAAC;AAAA,EAC1C;AACA,SAAM,OAAO,MAAM,QAAQ,QAAQ,SAAS,OAAO,QAAQ,MAAM;AACjE,SAAO;AACT;AAoBO,IAAM,SACX,SAAU,OAAO;AACf,MAAI,QAAQ,KAAK;AAAG,WAAO,CAAC;AAC5B,MAAI,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,KAAK,GAAG;AAC5C,WAAO,OAAO,OAAO,CAAC,GAAG,KAAK;AAAA,EAChC;AACA,QAAM,eAAe,QAAQ,KAAK;AACpC;AAGK,IAAM,WACX,CAAC,SAAY,SAAkB,aAAoE;AACnG,mBAAkB,OAAO;AACvB,UAAM,IAAI,OAAO,KAAK;AACtB,UAAM,YAAY,OAAO,KAAK,OAAO;AACrC,UAAM,cAAc,OAAO,KAAK,CAAC,EAAE,KAAK,UAAQ,CAAC,UAAU,SAAS,IAAI,CAAC;AACzE,QAAI,aAAa;AACf,YAAM,eACJ,SACA,OACA,QACA,4BAA4B,mBAAmB,aACjD;AAAA,IACF;AACA,UAAM,YAAY,UAAU,KAAK,cAAY;AAC3C,YAAM,iBAAiB,QAAQ;AAC/B,aAAQ,eAAe,SAAS,WAAW,CAAC,EAAE,eAAe,QAAQ;AAAA,IACvE,CAAC;AACD,QAAI,WAAW;AACb,YAAM,eACJ,SACA,EAAE,YACF,GAAG,UAAU,aACb,0BAA0B,kBAAkB,eAC5C,iBAAiB,QAAQ,QAAQ,UAAU,EAAE,OAAO,CAAC,KACrD,GACF;AAAA,IACF;AAEA,UAAM,UAAU,QAAQ,KAAK,IACzB,CAAC,KAAK,QAAQ,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,CAAC,IAC/D,CAAC,KAAK,QAAQ;AACd,YAAM,SAAS,QAAQ;AACvB,UAAI,OAAO,SAAS,cAAc,CAAC,EAAE,eAAe,GAAG,GAAG;AACxD,eAAO,OAAO,OAAO,KAAK,CAAC,CAAC;AAAA,MAC9B,OAAO;AACL,eAAO,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,OAAO,EAAE,MAAM,GAAG,UAAU,KAAK,EAAE,CAAC;AAAA,MACzE;AAAA,IACF;AACF,WAAO,UAAU,OAAO,SAAS,CAAC,CAAC;AAAA,EACrC;AACA,UAAQ,OAAO,MAAM;AACnB,UAAM,QAAQ,OAAO,KAAK,OAAO,EAAE,IACjC,CAAC,QAAQ,QAAQ,KAAK,SAAS,aAC3B,GAAG,SAAS,QAAQ,QAAQ,MAAM,EAAE,QAAQ,KAAK,CAAC,MAClD,GAAG,QAAQ,QAAQ,QAAQ,IAAI,GACrC;AACA,WAAO;AAAA,GAAQ,MAAM,KAAK,OAAO;AAAA;AAAA,EACnC;AACA,SAAO;AACT;AAGO,uBAAwB,aAAqB,SAAkB,UAAkB;AACtF,SAAO,SAAU,MAAW;AAC1B,WAAO,IAAI;AACX,eAAW,OAAO,MAAM;AACtB,kBAAY,OAAO,KAAK,MAAM,GAAG,UAAU,KAAK;AAAA,IAClD;AACA,WAAO;AAAA,EACT;AACF;AAEO,IAAM,WACR,CAAC,WAAsD;AACxD,QAAM,UAAU,QAAQ,QAAQ,KAAK;AACrC,qBAAmB,GAAG;AACpB,WAAO,QAAQ,CAAC;AAAA,EAClB;AACA,YAAS,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,QAAQ,OAAO,IAAI,QAAQ,MAAM;AAC3E,SAAO;AACT;AAUK,eAAgB,OAAO,SAAS,IAAI;AACzC,MAAI,QAAQ,KAAK,KAAK,QAAQ,KAAK;AAAG,WAAO;AAC7C,QAAM,eAAe,OAAO,OAAO,MAAM;AAC3C;AACA,MAAM,OAAO,MAAM;AAYZ,IAAM,SACX,iBAAiB,OAAO,SAAS,IAAI;AACnC,MAAI,QAAQ,KAAK;AAAG,WAAO;AAC3B,MAAI,SAAS,KAAK;AAAG,WAAO;AAC5B,QAAM,eAAe,SAAQ,OAAO,MAAM;AAC5C;AAIK,IAAM,SACX,iBAAiB,OAAO,SAAS,IAAI;AACnC,MAAI,QAAQ,KAAK;AAAG,WAAO;AAC3B,MAAI,SAAS,KAAK;AAAG,WAAO;AAC5B,QAAM,eAAe,SAAQ,OAAO,MAAM;AAC5C;AAkDF,qBAAsB,WAAW;AAC/B,iBAAgB,OAAc,SAAS,IAAI;AACzC,eAAW,UAAU,WAAW;AAC9B,UAAI;AACF,eAAO,OAAO,OAAO,MAAM;AAAA,MAC7B,SAAS,GAAP;AAAA,MAAW;AAAA,IACf;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,IAAI,UAAU,IAAI,QAAM,QAAQ,EAAE,CAAC,EAAE,KAAK,KAAK;AAClE,SAAO;AACT;AAGO,IAAM,UAAU;;;AC/XhB,IAAM,aAAkB,SAAS;AAAA,EACtC,cAAc;AAAA,EACd,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS,SAAS,MAAM;AAAA,EACxB,QAAQ;AAAA,EACR,WAAW,MAAM,QAAQ,MAAM;AAAA,EAC/B,SAAS;AACX,CAAC;AAIM,IAAM,yBAA8B,SAAS;AAAA,EAClD,MAAM;AAAA,EACN,aAAa;AAAA,EACb,MAAM,QAAQ,GAAG,OAAO,OAAO,cAAc,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAAA,EACrE,cAAc,QAAQ,GAAG,OAAO,OAAO,sBAAsB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AACvF,CAAC;AAEM,IAAM,cAAmB,cAAc;AAAA,EAC5C,MAAM,QAAQ,GAAG,OAAO,OAAO,aAAa,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAAA,EACpE,MAAM;AAAA,EACN,cAAc,cAAc;AAAA,IAC1B,MAAM,QAAQ,GAAG,OAAO,OAAO,qBAAqB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAAA,IAC5E,QAAQ,MAAM,QAAQ,MAAM;AAAA,EAC9B,CAAC;AAAA,EACD,iBAAiB,SAAS;AAAA,IACxB,IAAI;AAAA,IACJ,MAAM;AAAA,EACR,CAAC;AAAA,EACD,WAAW,MAAM,QAAQ,QAAQ,MAAM,CAAC;AAAA,EACxC,eAAe,QAAQ,MAAM;AAE/B,CAAC;AAIM,IAAM,WAAgB,QAAQ,GAAG,CAAC,mBAAmB,oBAAoB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;;;AC/CjG,IAAM,cAAc;AACpB,IAAM,eAAe,KAAK;AAC1B,IAAM,cAAc,KAAK;AACzB,IAAM,gBAAgB,KAAK;;;ACL3B,uCAAwC,KAAa;AAC1D,MAAI,SAAS,0BAA0B,QAAQ,MAAM,GAAG;AAC1D;;;AC4CO,uBAAwB,EAAE,MAAM,MAAM,MAAM,SAExC;AACT,QAAM,EAAE,MAAM,MAAM,oBAAoB;AACxC,QAAM,EAAE,gBAAgB;AAExB,MAAI,aAAa;AAAA,IACf;AAAA,IACA,UAAU,IAAI,KAAK,WAAW,EAAE,YAAY;AAAA,IAC5C,IAAI;AAAA,IACJ,MAAM,KAAK;AAAA,EACb;AAEA,MAAI,SAAS,cAAc,MAAM;AAC/B,iBAAa,CAAC,kBAAkB,EAAE,GAAG,YAAY,KAAK,IAAI,EAAE,GAAG,YAAY,MAAM,gBAAgB;AAAA,EACnG,WAAW,SAAS,cAAc,MAAM;AAAA,EAExC,WAAW,SAAS,cAAc,cAAc;AAC9C,UAAM,SAAS;AAAA,MACb,aAAa,OAAO,WAAW;AAAA,MAC/B,oBAAoB,OAAO,WAAW;AAAA,MACtC,GAAG,KAAK;AAAA,IACV;AACA,WAAO,OAAO;AACd,iBAAa;AAAA,MACX,GAAG;AAAA,MACH,cAAc,EAAE,MAAM,KAAK,aAAa,MAAM,OAAO;AAAA,IACvD;AAAA,EACF,WAAW,SAAS,cAAc,aAAa;AAAA,EAE/C;AACA,SAAO;AACT;AAEA,6BAAqC,EAAE,cAEpC;AACD,QAAM,YAAY,eAAI,kBAAkB;AACxC,QAAM,cAAc,eAAI,oBAAoB;AAC5C,MAAI,eAAe,YAAY,mBAAmB;AAChD,mBAAI,qBAAqB,wBAAwB;AAAA,MAC/C,SAAS,UAAU;AAAA,IACrB,CAAC;AACD,UAAM,eAAe,eAAI,mBAAmB,EAAE,QAAQ,QAAQ;AAC9D,QAAI,iBAAiB,eAAe,iBAAiB,yBAAyB;AAC5E,YAAM,eAAI,mBAAmB,EAC1B,KAAK,EAAE,MAAM,yBAAyB,QAAQ,EAAE,YAAY,YAAY,kBAAkB,EAAE,CAAC,EAC7F,MAAM,6BAA6B;AAAA,IACxC;AAAA,EACF;AAEA,iBAAI,qBAAqB,wBAAwB,EAAE,YAAY,WAAW,CAAC;AAC3E,iBAAI,qBAAqB,gCAAgC,EAAE,YAAY,WAAW,CAAC;AAKnF,iBAAI,4BAA4B,UAAU,EAAE,MAAM,OAAK;AACrD,YAAQ,MAAM,iBAAiB,6BAA6B,EAAE,SAAS,CAAC;AAAA,EAC1E,CAAC;AACH;AAEO,wBAAyB,IAAY,UAAiC;AAC3E,WAAS,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;AAC7C,QAAI,SAAS,GAAG,OAAO,IAAI;AACzB,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEO,iCAAkC,UAEvC;AACA,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,KAAK;AAAA,EACP;AACF;;;ACzGO,0BAA2B,EAAE,OAAO,MAAM,MAAM,QAE9C;AACP,QAAM,sBAAsB,eAAI,kBAAkB,EAAE;AACpD,MAAI,OAAO,iBAAiB,eAAe,aAAa,eAAe,aAAa,CAAC,qBAAqB;AACxG;AAAA,EACF;AAEA,QAAM,eAAe,IAAI,aAAa,OAAO,EAAE,MAAM,KAAK,CAAC;AAC3D,MAAI,MAAM;AACR,iBAAa,UAAU,SAAU,OAAO;AACtC,YAAM,eAAe;AACrB,qBAAI,mBAAmB,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,QAAQ,IAAI;AAAA,IAC5D;AAAA,EACF;AACF;;;AChBA,gCACE,kBACA,aAAqB,CAAC,GACd;AACR,SAAO;AAAA,IACL,MAAM,cAAc;AAAA,IACpB,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,GAAG;AAAA,IACL;AAAA,EACF;AACF;AAEA,0BAA2B,EAAE,YAAY,QAGhC;AACP,iBAAI,yBAAyB,GAAG,2BAA2B,cAAc,EAAE,KAAK,CAAC;AACnF;AAEA,oBAAqB,EAAE,YAAY,WAAW,UAAU,MAAM,UAAU,gBAO/D;AAIP,MAAI,eAAI,sBAAsB,wBAAwB,GAAG;AACvD;AAAA,EACF;AAEA,iBAAI,qBAAqB,4BAA4B;AAAA,IACnD,YAAY;AAAA,IACZ;AAAA,IACA,aAAa;AAAA,EACf,CAAC;AAED,QAAM,cAAc,eAAI,oBAAoB;AAC5C,QAAM,UAAU,YAAY,sBAAsB,UAAU;AAC5D,QAAM,OAAO,eAAe;AAE5B,mBAAiB;AAAA,IACf,OAAO,KAAK;AAAA,IACZ,MAAM;AAAA,IACN,MAAM,YAAY,eAAe,SAAS,QAAQ,EAAE;AAAA,IACpD;AAAA,EACF,CAAC;AAED,iBAAI,yBAAyB,eAAe;AAC9C;AAEA,uBAAwB,EAAE,YAAY,aAE7B;AACP,iBAAI,qBAAqB,+BAA+B,EAAE,YAAY,YAAY,UAAU,CAAC;AAC/F;AAEA,8BAA+B,EAAE,YAAY,MAAM,eAE1C;AACP,iBAAI,qBAAqB,0BAA0B;AAAA,IACjD,YAAY;AAAA,IACZ,WAAW;AAAA,IACX;AAAA,EACF,CAAC;AACH;AAEA,eAAI,2BAA2B;AAAA,EAC7B,MAAM;AAAA,EACN,UAAU;AAAA,IACR,UAAU,SAAS;AAAA,MACjB,aAAa;AAAA,MACb,UAAU;AAAA,MACV,oBAAoB;AAAA,IACtB,CAAC;AAAA,IACD,SAAU;AACR,YAAM,EAAE,UAAU,uBAAuB,eAAI,kBAAkB,EAAE;AACjE,aAAO;AAAA,QACL,aAAa,IAAI,KAAK,EAAE,YAAY;AAAA,QACpC;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,qBAAsB,OAAO;AAC3B,aAAO;AAAA,IACT;AAAA,IACA,iBAAkB,OAAO,SAAS;AAChC,aAAO,QAAQ,qBAAqB,YAAY,CAAC;AAAA,IACnD;AAAA,IACA,mBAAoB,OAAO,SAAS;AAClC,aAAO,QAAQ,qBAAqB,cAAc,CAAC;AAAA,IACrD;AAAA,IACA,cAAe,OAAO,SAAS;AAC7B,aAAO,QAAQ,qBAAqB,SAAS,CAAC;AAAA,IAChD;AAAA,IACA,uBAAwB,OAAO,SAAS;AACtC,aAAO,QAAQ,qBAAqB,YAAY,CAAC;AAAA,IACnD;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IAEP,yBAAyB;AAAA,MACvB,UAAU,SAAS;AAAA,QACjB,YAAY;AAAA,MACd,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAClC,cAAM,eAAe,MAAM;AAAA,UACzB,UAAU;AAAA,YACR,gBAAgB;AAAA,YAChB,iBAAiB;AAAA,YACjB,eAAe;AAAA,YACf,sBAAsB;AAAA,UACxB;AAAA,UACA,YAAY;AAAA,YACV,SAAS,KAAK;AAAA,YACd,aAAa;AAAA,YACb,cAAc;AAAA,UAChB;AAAA,UACA,OAAO,CAAC;AAAA,UACR,UAAU,CAAC;AAAA,QACb,GAAG,IAAI;AACP,mBAAW,OAAO,cAAc;AAC9B,mBAAI,IAAI,OAAO,KAAK,aAAa,IAAI;AAAA,QACvC;AAAA,MACF;AAAA,IACF;AAAA,IACA,8BAA8B;AAAA,MAC5B,UAAU,SAAS;AAAA,QACjB,UAAU;AAAA,MACZ,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,MAAM,QAAQ,EAAE,SAAS;AACxC,cAAM,EAAE,aAAa;AACrB,YAAI,CAAC,MAAM,eAAe,MAAM,MAAM,WAAW;AAE/C,kBAAQ,KAAK,yDAAyD;AACtE;AAAA,QACF;AAEA,iBAAI,IAAI,MAAM,OAAO,UAAU,EAAE,YAAY,KAAK,YAAY,CAAC;AAE/D,YAAI,CAAC,MAAM,aAAa;AACtB;AAAA,QACF;AAEA,cAAM,mBAAmB,aAAa,KAAK,WAAW,sBAAsB,cAAc,sBAAsB;AAChH,cAAM,mBAAmB,uBACvB,kBACA,qBAAqB,sBAAsB,aAAa,EAAE,SAAS,IAAI,CAAC,CAC1E;AACA,cAAM,aAAa,cAAc,EAAE,MAAM,MAAM,MAAM,kBAAkB,MAAM,CAAC;AAC9E,cAAM,SAAS,KAAK,UAAU;AAAA,MAChC;AAAA,MACA,WAAY,EAAE,YAAY,MAAM,QAAQ;AACtC,yBAAiB,EAAE,YAAY,KAAK,CAAC;AAErC,YAAI,eAAI,sBAAsB,wBAAwB,KACpD,eAAI,sBAAsB,qBAAqB,MAAM,YAAY;AACjE,+BAAqB,EAAE,YAAY,MAAM,aAAa,KAAK,YAAY,CAAC;AAAA,QAC1E;AAAA,MACF;AAAA,IACF;AAAA,IACA,gCAAgC;AAAA,MAC9B,UAAU,SAAS;AAAA,QACjB,MAAM;AAAA,MACR,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,MAAM,QAAQ,EAAE,SAAS;AACxC,iBAAI,IAAI,MAAM,YAAY,QAAQ,KAAK,IAAI;AAE3C,YAAI,CAAC,MAAM,aAAa;AACtB;AAAA,QACF;AAEA,cAAM,mBAAmB,uBAAuB,sBAAsB,aAAa,CAAC,CAAC;AACrF,cAAM,aAAa,cAAc,EAAE,MAAM,MAAM,MAAM,kBAAkB,MAAM,CAAC;AAC9E,cAAM,SAAS,KAAK,UAAU;AAAA,MAChC;AAAA,MACA,WAAY,EAAE,YAAY,MAAM,QAAQ;AACtC,yBAAiB,EAAE,YAAY,KAAK,CAAC;AAErC,YAAI,eAAI,sBAAsB,wBAAwB,GAAG;AACvD,+BAAqB,EAAE,YAAY,MAAM,aAAa,KAAK,YAAY,CAAC;AAAA,QAC1E;AAAA,MACF;AAAA,IACF;AAAA,IACA,2CAA2C;AAAA,MACzC,UAAU,SAAS;AAAA,QACjB,aAAa;AAAA,MACf,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,MAAM,QAAQ,EAAE,SAAS;AACxC,iBAAI,IAAI,MAAM,YAAY,eAAe,KAAK,WAAW;AAEzD,YAAI,CAAC,MAAM,aAAa;AACtB;AAAA,QACF;AAEA,cAAM,mBAAmB,uBACvB,sBAAsB,oBAAoB,CAAC,CAC7C;AACA,cAAM,aAAa,cAAc,EAAE,MAAM,MAAM,MAAM,kBAAkB,MAAM,CAAC;AAC9E,cAAM,SAAS,KAAK,UAAU;AAAA,MAChC;AAAA,MACA,WAAY,EAAE,YAAY,MAAM,QAAQ;AACtC,yBAAiB,EAAE,YAAY,KAAK,CAAC;AAErC,YAAI,eAAI,sBAAsB,wBAAwB,GAAG;AACvD,+BAAqB,EAAE,YAAY,MAAM,aAAa,KAAK,YAAY,CAAC;AAAA,QAC1E;AAAA,MACF;AAAA,IACF;AAAA,IACA,+BAA+B;AAAA,MAC7B,UAAU,SAAS;AAAA,QACjB,UAAU,SAAS,MAAM;AAAA,QACzB,QAAQ;AAAA,MACV,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,MAAM,QAAQ,EAAE,SAAS;AACxC,cAAM,EAAE,WAAW;AACnB,cAAM,WAAW,KAAK,YAAY,WAAW,KAAK;AAClD,YAAI,CAAC,MAAM,eAAe,CAAC,MAAM,MAAM,SAAS;AAC9C,gBAAM,IAAI,MAAM,oCAAoC,wBAAwB;AAAA,QAC9E;AACA,iBAAI,OAAO,MAAM,OAAO,MAAM;AAE9B,YAAI,CAAC,MAAM,aAAa;AACtB;AAAA,QACF;AAEA,cAAM,mBAAmB,CAAC,WAAW,sBAAsB,eAAe,sBAAsB;AAChG,cAAM,mBAAmB,uBAAuB,kBAAkB,WAAW,EAAE,UAAU,OAAO,IAAI,CAAC,CAAC;AACtG,cAAM,aAAa,cAAc;AAAA,UAC/B,MAAM,WAAW,OAAO,EAAE,GAAG,MAAM,UAAU,OAAO;AAAA,UACpD;AAAA,UACA,MAAM;AAAA,UACN;AAAA,QACF,CAAC;AACD,cAAM,SAAS,KAAK,UAAU;AAAA,MAChC;AAAA,MACA,WAAY,EAAE,MAAM,MAAM,YAAY,QAAQ,EAAE,SAAS;AACvD,cAAM,YAAY,eAAI,kBAAkB;AACxC,YAAI,KAAK,WAAW,UAAU,SAAS,UAAU;AAC/C,cAAI,eAAI,sBAAsB,wBAAwB,GAAG;AACvD,iCAAqB,EAAE,YAAY,MAAM,aAAa,KAAK,YAAY,CAAC;AAAA,UAC1E;AACA,cAAI,eAAI,sBAAsB,qBAAqB,GAAG;AACpD;AAAA,UACF;AACA,wBAAc,EAAE,WAAW,CAAC;AAAA,QAC9B;AACA,yBAAiB,EAAE,YAAY,KAAK,CAAC;AAAA,MACvC;AAAA,IACF;AAAA,IACA,gCAAgC;AAAA,MAC9B,UAAU,CAAC,MAAM,EAAE,OAAO,WAAW;AACnC,YAAI,MAAM,WAAW,YAAY,KAAK,UAAU;AAC9C,gBAAM,IAAI,UAAU,EAAE,8CAA8C,CAAC;AAAA,QACvE;AAAA,MACF;AAAA,MACA,QAAS,EAAE,MAAM,QAAQ,EAAE,OAAO,aAAa;AAC7C,iBAAI,IAAI,MAAM,YAAY,eAAe,KAAK,WAAW;AACzD,mBAAW,YAAY,MAAM,OAAO;AAClC,mBAAI,OAAO,MAAM,OAAO,QAAQ;AAAA,QAClC;AAAA,MACF;AAAA,MACA,WAAY,EAAE,MAAM,cAAc,EAAE,SAAS;AAC3C,YAAI,eAAI,sBAAsB,qBAAqB,GAAG;AACpD;AAAA,QACF;AACA,sBAAc,EAAE,WAAW,CAAC;AAAA,MAC9B;AAAA,IACF;AAAA,IACA,oCAAoC;AAAA,MAClC,UAAU;AAAA,MACV,QAAS,EAAE,MAAM,MAAM,QAAQ,EAAE,SAAS;AACxC,YAAI,CAAC,MAAM,aAAa;AACtB;AAAA,QACF;AACA,cAAM,aAAa,MAAM,SAAS,KAAK,SAAO,IAAI,OAAO,QAAQ,IAAI,OAAO;AAC5E,YAAI,YAAY;AACd,iBAAO,WAAW;AAAA,QACpB,OAAO;AACL,gBAAM,SAAS,KAAK,cAAc,EAAE,MAAM,MAAM,MAAM,MAAM,CAAC,CAAC;AAAA,QAChE;AAAA,MACF;AAAA,MACA,WAAY,EAAE,YAAY,MAAM,MAAM,QAAQ,EAAE,OAAO,WAAW;AAChE,yBAAiB,EAAE,YAAY,KAAK,CAAC;AAErC,cAAM,YAAY,eAAI,kBAAkB;AACxC,cAAM,KAAK,UAAU,SAAS;AAE9B,YAAI,OAAO,KAAK,UAAU;AACxB;AAAA,QACF;AACA,cAAM,aAAa,cAAc,EAAE,MAAM,MAAM,MAAM,MAAM,CAAC;AAC5D,cAAM,WAAW,wBAAwB,EAAE;AAC3C,YAAI,KAAK,SAAS,cAAc,QAC7B,YAAW,KAAK,SAAS,SAAS,EAAE,KAAK,WAAW,KAAK,SAAS,SAAS,GAAG,IAAI;AACnF,qBAAW;AAAA,YACT;AAAA,YACA,WAAW,WAAW;AAAA,YACtB,UAAU,WAAW;AAAA,YACrB,MAAM,WAAW;AAAA,YACjB,UAAU,KAAK;AAAA,YACf,cAAc,QAAQ,mBAAmB;AAAA,UAC3C,CAAC;AAAA,QACH;AAEA,YAAI,eAAI,sBAAsB,wBAAwB,GAAG;AACvD,+BAAqB,EAAE,YAAY,MAAM,aAAa,KAAK,YAAY,CAAC;AAAA,QAC1E;AAAA,MACF;AAAA,IACF;AAAA,IACA,qCAAqC;AAAA,MACnC,UAAU,CAAC,MAAM,EAAE,OAAO,WAAW;AACnC,iBAAS;AAAA,UACP,IAAI;AAAA,UACJ,aAAa;AAAA,UACb,MAAM;AAAA,QACR,CAAC,EAAE,IAAI;AAAA,MAIT;AAAA,MACA,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAClC,YAAI,CAAC,MAAM,aAAa;AACtB;AAAA,QACF;AACA,cAAM,WAAW,eAAe,KAAK,IAAI,MAAM,QAAQ;AACvD,YAAI,YAAY,KAAK,KAAK,aAAa,MAAM,SAAS,UAAU,MAAM;AACpE,gBAAM,SAAS,UAAU,OAAO,KAAK;AACrC,gBAAM,SAAS,UAAU,cAAc,KAAK;AAC5C,cAAI,MAAM,eAAe,MAAM,SAAS,UAAU,SAAS;AACzD,mBAAO,MAAM,SAAS,UAAU;AAAA,UAClC;AAAA,QACF;AAAA,MACF;AAAA,MACA,WAAY,EAAE,YAAY,MAAM,MAAM,QAAQ,EAAE,WAAW;AACzD,yBAAiB,EAAE,YAAY,KAAK,CAAC;AAErC,cAAM,YAAY,eAAI,kBAAkB;AACxC,cAAM,KAAK,UAAU,SAAS;AAE9B,YAAI,OAAO,KAAK,UAAU;AACxB;AAAA,QACF;AACA,cAAM,iBAAiB,UAAU,eAAe,YAAY,SAAS,KAAK,OAAK,EAAE,cAAc,KAAK,EAAE;AACtG,cAAM,WAAW,wBAAwB,EAAE;AAC3C,cAAM,mBAAmB,KAAK,KAAK,SAAS,SAAS,EAAE,KAAK,KAAK,KAAK,SAAS,SAAS,GAAG;AAC3F,YAAI,CAAC,kBAAkB,kBAAkB;AACvC,qBAAW;AAAA,YACT;AAAA,YACA,WAAW,KAAK;AAAA,YAOhB,UAAU,KAAK;AAAA,YACf,MAAM,KAAK;AAAA,YACX,UAAU,KAAK;AAAA,YACf,cAAc,QAAQ,mBAAmB;AAAA,UAC3C,CAAC;AAAA,QACH,WAAW,kBAAkB,CAAC,kBAAkB;AAC9C,wBAAc,EAAE,YAAY,WAAW,KAAK,GAAG,CAAC;AAAA,QAClD;AAAA,MACF;AAAA,IACF;AAAA,IACA,uCAAuC;AAAA,MACrC,UAAU,SAAS;AAAA,QACjB,IAAI;AAAA,MACN,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAClC,YAAI,CAAC,MAAM,aAAa;AACtB;AAAA,QACF;AACA,cAAM,WAAW,eAAe,KAAK,IAAI,MAAM,QAAQ;AACvD,YAAI,YAAY,GAAG;AACjB,gBAAM,SAAS,OAAO,UAAU,CAAC;AAAA,QACnC;AAEA,mBAAW,WAAW,MAAM,UAAU;AACpC,cAAI,QAAQ,iBAAiB,OAAO,KAAK,IAAI;AAC3C,oBAAQ,gBAAgB,KAAK;AAC7B,oBAAQ,gBAAgB,OAAO;AAAA,UACjC;AAAA,QACF;AAAA,MACF;AAAA,MACA,WAAY,EAAE,MAAM,YAAY,MAAM,QAAQ;AAC5C,yBAAiB,EAAE,YAAY,KAAK,CAAC;AAErC,cAAM,YAAY,eAAI,kBAAkB;AACxC,cAAM,KAAK,UAAU,SAAS;AAE9B,YAAI,UAAU,uBAAuB,gBAAgB,KAAK,IAAI;AAC5D,yBAAI,qBAAqB,6BAA6B;AAAA,YACpD,YAAY;AAAA,YAAY,WAAW;AAAA,UACrC,CAAC;AAAA,QACH;AAEA,YAAI,UAAU,eAAe,YAAY,MAAM,cAAc,KAAK,IAAI;AACpE,yBAAI,qBAAqB,6BAA6B;AAAA,YACpD,YAAY;AAAA,YACZ,aAAa,KAAK;AAAA,UACpB,CAAC;AAAA,QACH;AAEA,YAAI,OAAO,KAAK,UAAU;AACxB;AAAA,QACF;AACA,YAAI,UAAU,eAAe,YAAY,SAAS,KAAK,OAAK,EAAE,cAAc,KAAK,EAAE,GAAG;AACpF,wBAAc,EAAE,YAAY,WAAW,KAAK,GAAG,CAAC;AAAA,QAClD;AAEA,yBAAiB,EAAE,YAAY,KAAK,CAAC;AAAA,MACvC;AAAA,IACF;AAAA,IACA,qCAAqC;AAAA,MACnC,UAAU,SAAS;AAAA,QACjB,IAAI;AAAA,QACJ,UAAU;AAAA,MACZ,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,MAAM,cAAc,EAAE,SAAS;AAC9C,YAAI,CAAC,MAAM,aAAa;AACtB;AAAA,QACF;AACA,cAAM,EAAE,IAAI,aAAa;AACzB,cAAM,WAAW,eAAe,IAAI,MAAM,QAAQ;AAClD,YAAI,YAAY,GAAG;AACjB,cAAI,YAAY,UAAU,MAAM,SAAS,UAAU,aAAa,CAAC,CAAC;AAClE,cAAI,UAAU,WAAW;AACvB,kBAAM,eAAe,UAAU,UAAU,QAAQ,KAAK,QAAQ;AAC9D,gBAAI,gBAAgB,GAAG;AACrB,wBAAU,UAAU,OAAO,cAAc,CAAC;AAC1C,kBAAI,CAAC,UAAU,UAAU,QAAQ;AAC/B,uBAAO,UAAU;AACjB,oBAAI,CAAC,OAAO,KAAK,SAAS,EAAE,QAAQ;AAClC,8BAAY;AAAA,gBACd;AAAA,cACF;AAAA,YACF,OAAO;AACL,wBAAU,UAAU,KAAK,KAAK,QAAQ;AAAA,YACxC;AAAA,UACF,OAAO;AACL,sBAAU,YAAY,CAAC,KAAK,QAAQ;AAAA,UACtC;AACA,cAAI,WAAW;AACb,qBAAI,IAAI,MAAM,SAAS,WAAW,aAAa,SAAS;AAAA,UAC1D,OAAO;AACL,qBAAI,OAAO,MAAM,SAAS,WAAW,WAAW;AAAA,UAClD;AAAA,QACF;AAAA,MACF;AAAA,MACA,WAAY,EAAE,YAAY,QAAQ;AAChC,yBAAiB,EAAE,YAAY,KAAK,CAAC;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AACF,CAAC;", + "names": [] +} diff --git a/test/contracts/group.js b/test/contracts/group.js new file mode 100644 index 0000000000..33146eda3d --- /dev/null +++ b/test/contracts/group.js @@ -0,0 +1,1874 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; + +// node_modules/@sbp/sbp/dist/module.mjs +var selectors = {}; +var domains = {}; +var globalFilters = []; +var domainFilters = {}; +var selectorFilters = {}; +var unsafeSelectors = {}; +var DOMAIN_REGEX = /^[^/]+/; +function sbp(selector, ...data) { + const domain = domainFromSelector(selector); + if (!selectors[selector]) { + throw new Error(`SBP: selector not registered: ${selector}`); + } + for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) { + if (filters) { + for (const filter of filters) { + if (filter(domain, selector, data) === false) + return; + } + } + } + return selectors[selector].call(domains[domain].state, ...data); +} +function domainFromSelector(selector) { + const domainLookup = DOMAIN_REGEX.exec(selector); + if (domainLookup === null) { + throw new Error(`SBP: selector missing domain: ${selector}`); + } + return domainLookup[0]; +} +var SBP_BASE_SELECTORS = { + "sbp/selectors/register": function(sels) { + const registered = []; + for (const selector in sels) { + const domain = domainFromSelector(selector); + if (selectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`); + } else if (typeof sels[selector] === "function") { + if (unsafeSelectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`); + } + const fn = selectors[selector] = sels[selector]; + registered.push(selector); + if (!domains[domain]) { + domains[domain] = { state: {} }; + } + if (selector === `${domain}/_init`) { + fn.call(domains[domain].state); + } + } + } + return registered; + }, + "sbp/selectors/unregister": function(sels) { + for (const selector of sels) { + if (!unsafeSelectors[selector]) { + throw new Error(`SBP: can't unregister locked selector: ${selector}`); + } + delete selectors[selector]; + } + }, + "sbp/selectors/overwrite": function(sels) { + sbp("sbp/selectors/unregister", Object.keys(sels)); + return sbp("sbp/selectors/register", sels); + }, + "sbp/selectors/unsafe": function(sels) { + for (const selector of sels) { + if (selectors[selector]) { + throw new Error("unsafe must be called before registering selector"); + } + unsafeSelectors[selector] = true; + } + }, + "sbp/selectors/lock": function(sels) { + for (const selector of sels) { + delete unsafeSelectors[selector]; + } + }, + "sbp/selectors/fn": function(sel) { + return selectors[sel]; + }, + "sbp/filters/global/add": function(filter) { + globalFilters.push(filter); + }, + "sbp/filters/domain/add": function(domain, filter) { + if (!domainFilters[domain]) + domainFilters[domain] = []; + domainFilters[domain].push(filter); + }, + "sbp/filters/selector/add": function(selector, filter) { + if (!selectorFilters[selector]) + selectorFilters[selector] = []; + selectorFilters[selector].push(filter); + } +}; +SBP_BASE_SELECTORS["sbp/selectors/register"](SBP_BASE_SELECTORS); +var module_default = sbp; + +// frontend/common/common.js +import { default as default2 } from "vue"; + +// frontend/common/vSafeHtml.js +import dompurify from "dompurify"; +import Vue from "vue"; + +// frontend/model/contracts/shared/giLodash.js +function omit(o, props) { + const x = {}; + for (const k in o) { + if (!props.includes(k)) { + x[k] = o[k]; + } + } + return x; +} +function cloneDeep(obj) { + return JSON.parse(JSON.stringify(obj)); +} +function isMergeableObject(val) { + const nonNullObject = val && typeof val === "object"; + return nonNullObject && Object.prototype.toString.call(val) !== "[object RegExp]" && Object.prototype.toString.call(val) !== "[object Date]"; +} +function merge(obj, src) { + for (const key in src) { + const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : void 0; + if (clone && isMergeableObject(obj[key])) { + merge(obj[key], clone); + continue; + } + obj[key] = clone || src[key]; + } + return obj; +} +function deepEqualJSONType(a, b) { + if (a === b) + return true; + if (a === null || b === null || typeof a !== typeof b) + return false; + if (typeof a !== "object") + return a === b; + if (Array.isArray(a)) { + if (a.length !== b.length) + return false; + } else if (a.constructor.name !== "Object") { + throw new Error(`not JSON type: ${a}`); + } + for (const key in a) { + if (!deepEqualJSONType(a[key], b[key])) + return false; + } + return true; +} + +// frontend/common/vSafeHtml.js +var defaultConfig = { + ALLOWED_ATTR: ["class"], + ALLOWED_TAGS: ["b", "br", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: true +}; +var transform = (el, binding) => { + if (binding.oldValue !== binding.value) { + let config = defaultConfig; + if (binding.arg === "a") { + config = cloneDeep(config); + config.ALLOWED_ATTR.push("href", "target"); + config.ALLOWED_TAGS.push("a"); + } + el.textContent = ""; + el.appendChild(dompurify.sanitize(binding.value, config)); + } +}; +Vue.directive("safe-html", { + bind: transform, + update: transform +}); + +// frontend/common/translations.js +import dompurify2 from "dompurify"; +import Vue2 from "vue"; + +// frontend/common/stringTemplate.js +var nargs = /\{([0-9a-zA-Z_]+)\}/g; +function template(string3, ...args) { + const firstArg = args[0]; + const replacementsByKey = typeof firstArg === "object" && firstArg !== null ? firstArg : args; + return string3.replace(nargs, function replaceArg(match, capture, index) { + if (string3[index - 1] === "{" && string3[index + match.length] === "}") { + return capture; + } + const maybeReplacement = Object.prototype.hasOwnProperty.call(replacementsByKey, capture) ? replacementsByKey[capture] : void 0; + if (maybeReplacement === null || maybeReplacement === void 0) { + return ""; + } + return String(maybeReplacement); + }); +} + +// frontend/common/translations.js +Vue2.prototype.L = L; +Vue2.prototype.LTags = LTags; +var defaultLanguage = "en-US"; +var defaultLanguageCode = "en"; +var defaultTranslationTable = {}; +var dompurifyConfig = { + ...defaultConfig, + ALLOWED_ATTR: ["class", "href", "rel", "target"], + ALLOWED_TAGS: ["a", "b", "br", "button", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: false +}; +var currentLanguage = defaultLanguage; +var currentLanguageCode = defaultLanguage.split("-")[0]; +var currentTranslationTable = defaultTranslationTable; +module_default("sbp/selectors/register", { + "translations/init": async function init(language) { + const [languageCode] = language.toLowerCase().split("-"); + if (language.toLowerCase() === currentLanguage.toLowerCase()) + return; + if (languageCode === currentLanguageCode) + return; + if (languageCode === defaultLanguageCode) { + currentLanguage = defaultLanguage; + currentLanguageCode = defaultLanguageCode; + currentTranslationTable = defaultTranslationTable; + return; + } + try { + currentTranslationTable = await module_default("backend/translations/get", language) || defaultTranslationTable; + currentLanguage = language; + currentLanguageCode = languageCode; + } catch (error) { + console.error(error); + } + } +}); +function LTags(...tags) { + const o = { + "br_": "
" + }; + for (const tag of tags) { + o[`${tag}_`] = `<${tag}>`; + o[`_${tag}`] = ``; + } + return o; +} +function L(key, args) { + return template(currentTranslationTable[key] || key, args).replace(/\s(?=[;:?!])/g, " "); +} +function sanitize(inputString) { + return dompurify2.sanitize(inputString, dompurifyConfig); +} +Vue2.component("i18n", { + functional: true, + props: { + args: [Object, Array], + tag: { + type: String, + default: "span" + }, + compile: Boolean + }, + render: function(h, context) { + const text = context.children[0].text; + const translation = L(text, context.props.args || {}); + if (!translation) { + console.warn("The following i18n text was not translated correctly:", text); + return h(context.props.tag, context.data, text); + } + if (context.props.tag === "a" && context.data.attrs.target === "_blank") { + context.data.attrs.rel = "noopener noreferrer"; + } + if (context.props.compile) { + const result = Vue2.compile("" + sanitize(translation) + ""); + return result.render.call({ + _c: (tag, ...args) => { + if (tag === "wrap") { + return h(context.props.tag, context.data, ...args); + } else { + return h(tag, ...args); + } + }, + _v: (x) => x + }); + } else { + if (!context.data.domProps) + context.data.domProps = {}; + context.data.domProps.innerHTML = sanitize(translation); + return h(context.props.tag, context.data); + } + } +}); + +// frontend/common/errors.js +var errors_exports = {}; +__export(errors_exports, { + GIErrorIgnoreAndBan: () => GIErrorIgnoreAndBan, + GIErrorUIRuntimeError: () => GIErrorUIRuntimeError +}); +var GIErrorIgnoreAndBan = class extends Error { + constructor(...params) { + super(...params); + this.name = "GIErrorIgnoreAndBan"; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +}; +var GIErrorUIRuntimeError = class extends Error { + constructor(...params) { + super(...params); + this.name = "GIErrorUIRuntimeError"; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +}; + +// frontend/model/contracts/misc/flowTyper.js +var EMPTY_VALUE = Symbol("@@empty"); +var isEmpty = (v) => v === EMPTY_VALUE; +var isNil = (v) => v === null; +var isUndef = (v) => typeof v === "undefined"; +var isBoolean = (v) => typeof v === "boolean"; +var isNumber = (v) => typeof v === "number"; +var isString = (v) => typeof v === "string"; +var isObject = (v) => !isNil(v) && typeof v === "object"; +var isFunction = (v) => typeof v === "function"; +var getType = (typeFn, _options) => { + if (isFunction(typeFn.type)) + return typeFn.type(_options); + return typeFn.name || "?"; +}; +var TypeValidatorError = class extends Error { + expectedType; + valueType; + value; + typeScope; + sourceFile; + constructor(message, expectedType, valueType, value, typeName = "", typeScope = "") { + const errMessage = message || `invalid "${valueType}" value type; ${typeName || expectedType} type expected`; + super(errMessage); + this.expectedType = expectedType; + this.valueType = valueType; + this.value = value; + this.typeScope = typeScope || ""; + this.sourceFile = this.getSourceFile(); + this.message = `${errMessage} +${this.getErrorInfo()}`; + this.name = this.constructor.name; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, TypeValidatorError); + } + } + getSourceFile() { + const fileNames = this.stack.match(/(\/[\w_\-.]+)+(\.\w+:\d+:\d+)/g) || []; + return fileNames.find((fileName) => fileName.indexOf("/flowTyper-js/dist/") === -1) || ""; + } + getErrorInfo() { + return ` + file ${this.sourceFile} + scope ${this.typeScope} + expected ${this.expectedType.replace(/\n/g, "")} + type ${this.valueType} + value ${this.value} +`; + } +}; +var validatorError = (typeFn, value, scope, message, expectedType, valueType) => { + return new TypeValidatorError(message, expectedType || getType(typeFn), valueType || typeof value, JSON.stringify(value), typeFn.name, scope); +}; +var arrayOf = (typeFn, _scope = "Array") => { + function array(value) { + if (isEmpty(value)) + return [typeFn(value)]; + if (Array.isArray(value)) { + let index = 0; + return value.map((v) => typeFn(v, `${_scope}[${index++}]`)); + } + throw validatorError(array, value, _scope); + } + array.type = () => `Array<${getType(typeFn)}>`; + return array; +}; +var literalOf = (primitive) => { + function literal(value, _scope = "") { + if (isEmpty(value) || value === primitive) + return primitive; + throw validatorError(literal, value, _scope); + } + literal.type = () => { + if (isBoolean(primitive)) + return `${primitive ? "true" : "false"}`; + else + return `"${primitive}"`; + }; + return literal; +}; +var mapOf = (keyTypeFn, typeFn) => { + function mapOf2(value) { + if (isEmpty(value)) + return {}; + const o = object(value); + const reducer = (acc, key) => Object.assign(acc, { + [keyTypeFn(key, "Map[_]")]: typeFn(o[key], `Map.${key}`) + }); + return Object.keys(o).reduce(reducer, {}); + } + mapOf2.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`; + return mapOf2; +}; +var object = function(value) { + if (isEmpty(value)) + return {}; + if (isObject(value) && !Array.isArray(value)) { + return Object.assign({}, value); + } + throw validatorError(object, value); +}; +var objectOf = (typeObj, _scope = "Object") => { + function object2(value) { + const o = object(value); + const typeAttrs = Object.keys(typeObj); + const unknownAttr = Object.keys(o).find((attr) => !typeAttrs.includes(attr)); + if (unknownAttr) { + throw validatorError(object2, value, _scope, `missing object property '${unknownAttr}' in ${_scope} type`); + } + const undefAttr = typeAttrs.find((property) => { + const propertyTypeFn = typeObj[property]; + return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + }); + if (undefAttr) { + throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); + } + const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { + const typeFn = typeObj[key]; + if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + return Object.assign(acc, {}); + } else { + return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); + } + }; + return typeAttrs.reduce(reducer, {}); + } + object2.type = () => { + const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + return `{| + ${props.join(",\n ")} +|}`; + }; + return object2; +}; +function objectMaybeOf(validations, _scope = "Object") { + return function(data) { + object(data); + for (const key in data) { + validations[key]?.(data[key], `${_scope}.${key}`); + } + return data; + }; +} +var optional = (typeFn) => { + const unionFn = unionOf(typeFn, undef); + function optional2(v) { + return unionFn(v); + } + optional2.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn); + return optional2; +}; +function undef(value, _scope = "") { + if (isEmpty(value) || isUndef(value)) + return void 0; + throw validatorError(undef, value, _scope); +} +undef.type = () => "void"; +var boolean = function boolean2(value, _scope = "") { + if (isEmpty(value)) + return false; + if (isBoolean(value)) + return value; + throw validatorError(boolean2, value, _scope); +}; +var number = function number2(value, _scope = "") { + if (isEmpty(value)) + return 0; + if (isNumber(value)) + return value; + throw validatorError(number2, value, _scope); +}; +var string = function string2(value, _scope = "") { + if (isEmpty(value)) + return ""; + if (isString(value)) + return value; + throw validatorError(string2, value, _scope); +}; +function tupleOf_(...typeFuncs) { + function tuple(value, _scope = "") { + const cardinality = typeFuncs.length; + if (isEmpty(value)) + return typeFuncs.map((fn) => fn(value)); + if (Array.isArray(value) && value.length === cardinality) { + const tupleValue = []; + for (let i = 0; i < cardinality; i += 1) { + tupleValue.push(typeFuncs[i](value[i], _scope)); + } + return tupleValue; + } + throw validatorError(tuple, value, _scope); + } + tuple.type = () => `[${typeFuncs.map((fn) => getType(fn)).join(", ")}]`; + return tuple; +} +var tupleOf = tupleOf_; +function unionOf_(...typeFuncs) { + function union(value, _scope = "") { + for (const typeFn of typeFuncs) { + try { + return typeFn(value, _scope); + } catch (_) { + } + } + throw validatorError(union, value, _scope); + } + union.type = () => `(${typeFuncs.map((fn) => getType(fn)).join(" | ")})`; + return union; +} +var unionOf = unionOf_; + +// frontend/model/contracts/shared/constants.js +var INVITE_INITIAL_CREATOR = "invite-initial-creator"; +var INVITE_STATUS = { + REVOKED: "revoked", + VALID: "valid", + USED: "used" +}; +var PROFILE_STATUS = { + ACTIVE: "active", + PENDING: "pending", + REMOVED: "removed" +}; +var PROPOSAL_RESULT = "proposal-result"; +var PROPOSAL_INVITE_MEMBER = "invite-member"; +var PROPOSAL_REMOVE_MEMBER = "remove-member"; +var PROPOSAL_GROUP_SETTING_CHANGE = "group-setting-change"; +var PROPOSAL_PROPOSAL_SETTING_CHANGE = "proposal-setting-change"; +var PROPOSAL_GENERIC = "generic"; +var PROPOSAL_ARCHIVED = "proposal-archived"; +var MAX_ARCHIVED_PROPOSALS = 100; +var STATUS_OPEN = "open"; +var STATUS_PASSED = "passed"; +var STATUS_FAILED = "failed"; +var STATUS_CANCELLED = "cancelled"; +var CHATROOM_TYPES = { + INDIVIDUAL: "individual", + GROUP: "group" +}; +var CHATROOM_PRIVACY_LEVEL = { + GROUP: "chatroom-privacy-level-group", + PRIVATE: "chatroom-privacy-level-private", + PUBLIC: "chatroom-privacy-level-public" +}; +var MESSAGE_TYPES = { + POLL: "message-poll", + TEXT: "message-text", + INTERACTIVE: "message-interactive", + NOTIFICATION: "message-notification" +}; +var INVITE_EXPIRES_IN_DAYS = { + ON_BOARDING: 30, + PROPOSAL: 7 +}; +var MESSAGE_NOTIFICATIONS = { + ADD_MEMBER: "add-member", + JOIN_MEMBER: "join-member", + LEAVE_MEMBER: "leave-member", + KICK_MEMBER: "kick-member", + UPDATE_DESCRIPTION: "update-description", + UPDATE_NAME: "update-name", + DELETE_CHANNEL: "delete-channel", + VOTE: "vote" +}; +var MAIL_TYPE_MESSAGE = "message"; +var MAIL_TYPE_FRIEND_REQ = "friend-request"; + +// frontend/model/contracts/shared/voting/rules.js +var VOTE_AGAINST = ":against"; +var VOTE_INDIFFERENT = ":indifferent"; +var VOTE_UNDECIDED = ":undecided"; +var VOTE_FOR = ":for"; +var RULE_PERCENTAGE = "percentage"; +var RULE_DISAGREEMENT = "disagreement"; +var RULE_MULTI_CHOICE = "multi-choice"; +var getPopulation = (state) => Object.keys(state.profiles).filter((p) => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length; +var rules = { + [RULE_PERCENTAGE]: function(state, proposalType2, votes) { + votes = Object.values(votes); + let population = getPopulation(state); + if (proposalType2 === PROPOSAL_REMOVE_MEMBER) + population -= 1; + const defaultThreshold = state.settings.proposals[proposalType2].ruleSettings[RULE_PERCENTAGE].threshold; + const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population); + const totalIndifferent = votes.filter((x) => x === VOTE_INDIFFERENT).length; + const totalFor = votes.filter((x) => x === VOTE_FOR).length; + const totalAgainst = votes.filter((x) => x === VOTE_AGAINST).length; + const totalForOrAgainst = totalFor + totalAgainst; + const turnout = totalForOrAgainst + totalIndifferent; + const absent = population - turnout; + const neededToPass = Math.ceil(threshold * (population - totalIndifferent)); + console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType2}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population }); + if (totalFor >= neededToPass) { + return VOTE_FOR; + } + return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED; + }, + [RULE_DISAGREEMENT]: function(state, proposalType2, votes) { + votes = Object.values(votes); + const population = getPopulation(state); + const minimumMax = proposalType2 === PROPOSAL_REMOVE_MEMBER ? 2 : 1; + const thresholdOriginal = Math.max(state.settings.proposals[proposalType2].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax); + const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population); + const totalFor = votes.filter((x) => x === VOTE_FOR).length; + const totalAgainst = votes.filter((x) => x === VOTE_AGAINST).length; + const turnout = votes.length; + const absent = population - turnout; + console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType2}:`, { totalFor, totalAgainst, threshold, turnout, population, absent }); + if (totalAgainst >= threshold) { + return VOTE_AGAINST; + } + return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED; + }, + [RULE_MULTI_CHOICE]: function(state, proposalType2, votes) { + throw new Error("unimplemented!"); + } +}; +var rules_default = rules; +var ruleType = unionOf(...Object.keys(rules).map((k) => literalOf(k))); +var getThresholdAdjusted = (rule, threshold, groupSize) => { + const groupSizeVoting = Math.max(3, groupSize); + return { + [RULE_DISAGREEMENT]: () => { + return Math.min(groupSizeVoting - 1, threshold); + }, + [RULE_PERCENTAGE]: () => { + const minThreshold = 2 / groupSizeVoting; + return Math.max(minThreshold, threshold); + } + }[rule](); +}; + +// frontend/model/contracts/shared/time.js +var MINS_MILLIS = 6e4; +var HOURS_MILLIS = 60 * MINS_MILLIS; +var DAYS_MILLIS = 24 * HOURS_MILLIS; +var MONTHS_MILLIS = 30 * DAYS_MILLIS; +function dateToPeriodStamp(date) { + return new Date(date).toISOString(); +} +function dateFromPeriodStamp(daystamp) { + return new Date(daystamp); +} +function periodStampGivenDate({ recentDate, periodStart, periodLength }) { + const periodStartDate = dateFromPeriodStamp(periodStart); + let nextPeriod = addTimeToDate(periodStartDate, periodLength); + const curDate = new Date(recentDate); + let curPeriod; + if (curDate < nextPeriod) { + if (curDate >= periodStartDate) { + return periodStart; + } else { + curPeriod = periodStartDate; + do { + curPeriod = addTimeToDate(curPeriod, -periodLength); + } while (curDate < curPeriod); + } + } else { + do { + curPeriod = nextPeriod; + nextPeriod = addTimeToDate(nextPeriod, periodLength); + } while (curDate >= nextPeriod); + } + return dateToPeriodStamp(curPeriod); +} +function dateIsWithinPeriod({ date, periodStart, periodLength }) { + const dateObj = new Date(date); + const start = dateFromPeriodStamp(periodStart); + return dateObj > start && dateObj < addTimeToDate(start, periodLength); +} +function addTimeToDate(date, timeMillis) { + const d = new Date(date); + d.setTime(d.getTime() + timeMillis); + return d; +} +function comparePeriodStamps(periodA, periodB) { + return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime(); +} +function compareISOTimestamps(a, b) { + return new Date(a).getTime() - new Date(b).getTime(); +} +function isPeriodStamp(arg) { + return /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(arg); +} + +// frontend/model/contracts/shared/voting/proposals.js +function archiveProposal({ state, proposalHash, proposal, contractID }) { + default2.delete(state.proposals, proposalHash); + module_default("gi.contracts/group/pushSideEffect", contractID, ["gi.contracts/group/archiveProposal", contractID, proposalHash, proposal]); +} +var proposalSettingsType = objectOf({ + rule: ruleType, + expires_ms: number, + ruleSettings: objectOf({ + [RULE_PERCENTAGE]: objectOf({ threshold: number }), + [RULE_DISAGREEMENT]: objectOf({ threshold: number }) + }) +}); +function voteAgainst(state, { meta, data, contractID }) { + const { proposalHash } = data; + const proposal = state.proposals[proposalHash]; + proposal.status = STATUS_FAILED; + module_default("okTurtles.events/emit", PROPOSAL_RESULT, state, VOTE_AGAINST, data); + archiveProposal({ state, proposalHash, proposal, contractID }); +} +var proposalDefaults = { + rule: RULE_PERCENTAGE, + expires_ms: 14 * DAYS_MILLIS, + ruleSettings: { + [RULE_PERCENTAGE]: { threshold: 0.66 }, + [RULE_DISAGREEMENT]: { threshold: 1 } + } +}; +var proposals = { + [PROPOSAL_INVITE_MEMBER]: { + defaults: proposalDefaults, + [VOTE_FOR]: function(state, { meta, data, contractID }) { + const { proposalHash } = data; + const proposal = state.proposals[proposalHash]; + proposal.payload = data.passPayload; + proposal.status = STATUS_PASSED; + const message = { meta, data: data.passPayload, contractID }; + module_default("gi.contracts/group/invite/process", message, state); + module_default("okTurtles.events/emit", PROPOSAL_RESULT, state, VOTE_FOR, data); + archiveProposal({ state, proposalHash, proposal, contractID }); + }, + [VOTE_AGAINST]: voteAgainst + }, + [PROPOSAL_REMOVE_MEMBER]: { + defaults: proposalDefaults, + [VOTE_FOR]: function(state, { meta, data, contractID }) { + const { proposalHash, passPayload } = data; + const proposal = state.proposals[proposalHash]; + proposal.status = STATUS_PASSED; + proposal.payload = passPayload; + const messageData = { + ...proposal.data.proposalData, + proposalHash, + proposalPayload: passPayload + }; + const message = { data: messageData, meta, contractID }; + module_default("gi.contracts/group/removeMember/process", message, state); + module_default("gi.contracts/group/pushSideEffect", contractID, ["gi.contracts/group/removeMember/sideEffect", message]); + archiveProposal({ state, proposalHash, proposal, contractID }); + }, + [VOTE_AGAINST]: voteAgainst + }, + [PROPOSAL_GROUP_SETTING_CHANGE]: { + defaults: proposalDefaults, + [VOTE_FOR]: function(state, { meta, data, contractID }) { + const { proposalHash } = data; + const proposal = state.proposals[proposalHash]; + proposal.status = STATUS_PASSED; + const { setting, proposedValue } = proposal.data.proposalData; + const message = { + meta, + data: { [setting]: proposedValue }, + contractID + }; + module_default("gi.contracts/group/updateSettings/process", message, state); + archiveProposal({ state, proposalHash, proposal, contractID }); + }, + [VOTE_AGAINST]: voteAgainst + }, + [PROPOSAL_PROPOSAL_SETTING_CHANGE]: { + defaults: proposalDefaults, + [VOTE_FOR]: function(state, { meta, data, contractID }) { + const { proposalHash } = data; + const proposal = state.proposals[proposalHash]; + proposal.status = STATUS_PASSED; + const message = { + meta, + data: proposal.data.proposalData, + contractID + }; + module_default("gi.contracts/group/updateAllVotingRules/process", message, state); + archiveProposal({ state, proposalHash, proposal, contractID }); + }, + [VOTE_AGAINST]: voteAgainst + }, + [PROPOSAL_GENERIC]: { + defaults: proposalDefaults, + [VOTE_FOR]: function(state, { meta, data, contractID }) { + const { proposalHash } = data; + const proposal = state.proposals[proposalHash]; + proposal.status = STATUS_PASSED; + module_default("okTurtles.events/emit", PROPOSAL_RESULT, state, VOTE_FOR, data); + archiveProposal({ state, proposalHash, proposal, contractID }); + }, + [VOTE_AGAINST]: voteAgainst + } +}; +var proposals_default = proposals; +var proposalType = unionOf(...Object.keys(proposals).map((k) => literalOf(k))); + +// frontend/model/contracts/shared/payments/index.js +var PAYMENT_PENDING = "pending"; +var PAYMENT_CANCELLED = "cancelled"; +var PAYMENT_ERROR = "error"; +var PAYMENT_NOT_RECEIVED = "not-received"; +var PAYMENT_COMPLETED = "completed"; +var paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map((k) => literalOf(k))); +var PAYMENT_TYPE_MANUAL = "manual"; +var PAYMENT_TYPE_BITCOIN = "bitcoin"; +var PAYMENT_TYPE_PAYPAL = "paypal"; +var paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map((k) => literalOf(k))); + +// frontend/model/contracts/shared/distribution/mincome-proportional.js +function mincomeProportional(haveNeeds) { + let totalHave = 0; + let totalNeed = 0; + const havers = []; + const needers = []; + for (const haveNeed of haveNeeds) { + if (haveNeed.haveNeed > 0) { + havers.push(haveNeed); + totalHave += haveNeed.haveNeed; + } else if (haveNeed.haveNeed < 0) { + needers.push(haveNeed); + totalNeed += Math.abs(haveNeed.haveNeed); + } + } + const totalPercent = Math.min(1, totalNeed / totalHave); + const payments = []; + for (const haver of havers) { + const distributionAmount = totalPercent * haver.haveNeed; + for (const needer of needers) { + const belowPercentage = Math.abs(needer.haveNeed) / totalNeed; + payments.push({ + amount: distributionAmount * belowPercentage, + from: haver.name, + to: needer.name + }); + } + } + return payments; +} + +// frontend/model/contracts/shared/distribution/payments-minimizer.js +function minimizeTotalPaymentsCount(distribution) { + const neederTotalReceived = {}; + const haverTotalHave = {}; + const haversSorted = []; + const needersSorted = []; + const minimizedDistribution = []; + for (const todo of distribution) { + neederTotalReceived[todo.to] = (neederTotalReceived[todo.to] || 0) + todo.amount; + haverTotalHave[todo.from] = (haverTotalHave[todo.from] || 0) + todo.amount; + } + for (const name in haverTotalHave) { + haversSorted.push({ name, amount: haverTotalHave[name] }); + } + for (const name in neederTotalReceived) { + needersSorted.push({ name, amount: neederTotalReceived[name] }); + } + haversSorted.sort((a, b) => b.amount - a.amount); + needersSorted.sort((a, b) => b.amount - a.amount); + while (haversSorted.length > 0 && needersSorted.length > 0) { + const mostHaver = haversSorted.pop(); + const mostNeeder = needersSorted.pop(); + const diff = mostHaver.amount - mostNeeder.amount; + if (diff < 0) { + minimizedDistribution.push({ amount: mostHaver.amount, from: mostHaver.name, to: mostNeeder.name }); + mostNeeder.amount -= mostHaver.amount; + needersSorted.push(mostNeeder); + } else if (diff > 0) { + minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name }); + mostHaver.amount -= mostNeeder.amount; + haversSorted.push(mostHaver); + } else { + minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name }); + } + } + return minimizedDistribution; +} + +// frontend/model/contracts/shared/currencies.js +var DECIMALS_MAX = 8; +function commaToDots(value) { + return typeof value === "string" ? value.replace(/,/, ".") : value.toString(); +} +function isNumeric(nr) { + return !isNaN(nr - parseFloat(nr)); +} +function isInDecimalsLimit(nr, decimalsMax) { + const decimals = nr.split(".")[1]; + return !decimals || decimals.length <= decimalsMax; +} +function validateMincome(value, decimalsMax) { + const nr = commaToDots(value); + return isNumeric(nr) && isInDecimalsLimit(nr, decimalsMax); +} +function decimalsOrInt(num, decimalsMax) { + return num.toFixed(decimalsMax).replace(/\.0+$/, ""); +} +function saferFloat(value) { + return parseFloat(value.toFixed(DECIMALS_MAX)); +} +function makeCurrency(options) { + const { symbol, symbolWithCode, decimalsMax, formatCurrency } = options; + return { + symbol, + symbolWithCode, + decimalsMax, + displayWithCurrency: (n) => formatCurrency(decimalsOrInt(n, decimalsMax)), + displayWithoutCurrency: (n) => decimalsOrInt(n, decimalsMax), + validate: (n) => validateMincome(n, decimalsMax) + }; +} +var currencies = { + USD: makeCurrency({ + symbol: "$", + symbolWithCode: "$ USD", + decimalsMax: 2, + formatCurrency: (amount) => "$" + amount + }), + EUR: makeCurrency({ + symbol: "\u20AC", + symbolWithCode: "\u20AC EUR", + decimalsMax: 2, + formatCurrency: (amount) => "\u20AC" + amount + }), + BTC: makeCurrency({ + symbol: "\u0243", + symbolWithCode: "\u0243 BTC", + decimalsMax: DECIMALS_MAX, + formatCurrency: (amount) => amount + "\u0243" + }) +}; +var currencies_default = currencies; + +// frontend/model/contracts/shared/distribution/distribution.js +var tinyNum = 1 / Math.pow(10, DECIMALS_MAX); +function unadjustedDistribution({ haveNeeds = [], minimize = true }) { + const distribution = mincomeProportional(haveNeeds); + return minimize ? minimizeTotalPaymentsCount(distribution) : distribution; +} +function adjustedDistribution({ distribution, payments, dueOn }) { + distribution = cloneDeep(distribution); + for (const todo of distribution) { + todo.total = todo.amount; + } + distribution = subtractDistributions(distribution, payments).filter((todo) => todo.amount >= tinyNum); + for (const todo of distribution) { + todo.amount = saferFloat(todo.amount); + todo.total = saferFloat(todo.total); + todo.partial = todo.total !== todo.amount; + todo.isLate = false; + todo.dueOn = dueOn; + } + return distribution; +} +function reduceDistribution(payments) { + payments = cloneDeep(payments); + for (let i = 0; i < payments.length; i++) { + const paymentA = payments[i]; + for (let j = i + 1; j < payments.length; j++) { + const paymentB = payments[j]; + if (paymentA.from === paymentB.from && paymentA.to === paymentB.to || paymentA.to === paymentB.from && paymentA.from === paymentB.to) { + paymentA.amount += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.amount; + paymentA.total += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.total; + payments.splice(j, 1); + j--; + } + } + } + return payments; +} +function addDistributions(paymentsA, paymentsB) { + return reduceDistribution([...paymentsA, ...paymentsB]); +} +function subtractDistributions(paymentsA, paymentsB) { + paymentsB = cloneDeep(paymentsB); + for (const p of paymentsB) { + p.amount *= -1; + p.total *= -1; + } + return addDistributions(paymentsA, paymentsB); +} + +// frontend/model/contracts/shared/types.js +var inviteType = objectOf({ + inviteSecret: string, + quantity: number, + creator: string, + invitee: optional(string), + status: string, + responses: mapOf(string, string), + expires: number +}); +var chatRoomAttributesType = objectOf({ + name: string, + description: string, + type: unionOf(...Object.values(CHATROOM_TYPES).map((v) => literalOf(v))), + privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map((v) => literalOf(v))) +}); +var messageType = objectMaybeOf({ + type: unionOf(...Object.values(MESSAGE_TYPES).map((v) => literalOf(v))), + text: string, + notification: objectMaybeOf({ + type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map((v) => literalOf(v))), + params: mapOf(string, string) + }), + replyingMessage: objectOf({ + id: string, + text: string + }), + emoticons: mapOf(string, arrayOf(string)), + onlyVisibleTo: arrayOf(string) +}); +var mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map((k) => literalOf(k))); + +// frontend/model/contracts/group.js +function vueFetchInitKV(obj, key, initialValue) { + let value = obj[key]; + if (!value) { + default2.set(obj, key, initialValue); + value = obj[key]; + } + return value; +} +function initGroupProfile(contractID, joinedDate) { + return { + globalUsername: "", + contractID, + joinedDate, + nonMonetaryContributions: [], + status: PROFILE_STATUS.ACTIVE, + departedDate: null + }; +} +function initPaymentPeriod({ getters }) { + return { + initialCurrency: getters.groupMincomeCurrency, + mincomeExchangeRate: 1, + paymentsFrom: {}, + lastAdjustedDistribution: null, + haveNeedsSnapshot: null + }; +} +function clearOldPayments({ state, getters }) { + const sortedPeriodKeys = Object.keys(state.paymentsByPeriod).sort(); + while (sortedPeriodKeys.length > 2) { + const period = sortedPeriodKeys.shift(); + for (const paymentHash of getters.paymentHashesForPeriod(period)) { + default2.delete(state.payments, paymentHash); + } + default2.delete(state.paymentsByPeriod, period); + } +} +function initFetchPeriodPayments({ meta, state, getters }) { + const period = getters.periodStampGivenDate(meta.createdDate); + const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ getters })); + clearOldPayments({ state, getters }); + return periodPayments; +} +function updateCurrentDistribution({ meta, state, getters }) { + const curPeriodPayments = initFetchPeriodPayments({ meta, state, getters }); + const period = getters.periodStampGivenDate(meta.createdDate); + const noPayments = Object.keys(curPeriodPayments.paymentsFrom).length === 0; + if (comparePeriodStamps(period, getters.groupSettings.distributionDate) > 0) { + getters.groupSettings.distributionDate = period; + } + if (noPayments || !curPeriodPayments.haveNeedsSnapshot) { + curPeriodPayments.haveNeedsSnapshot = getters.haveNeedsForThisPeriod(period); + } + if (!noPayments) { + updateAdjustedDistribution({ period, getters }); + } +} +function updateAdjustedDistribution({ period, getters }) { + const payments = getters.groupPeriodPayments[period]; + if (payments && payments.haveNeedsSnapshot) { + const minimize = getters.groupSettings.minimizeDistribution; + payments.lastAdjustedDistribution = adjustedDistribution({ + distribution: unadjustedDistribution({ haveNeeds: payments.haveNeedsSnapshot, minimize }), + payments: getters.paymentsForPeriod(period), + dueOn: getters.dueDateForPeriod(period) + }).filter((todo) => { + return getters.groupProfile(todo.to).status === PROFILE_STATUS.ACTIVE; + }); + } +} +function memberLeaves({ username, dateLeft }, { meta, state, getters }) { + state.profiles[username].status = PROFILE_STATUS.REMOVED; + state.profiles[username].departedDate = dateLeft; + updateCurrentDistribution({ meta, state, getters }); +} +function isActionYoungerThanUser(actionMeta, userProfile) { + return Boolean(userProfile) && compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0; +} +module_default("chelonia/defineContract", { + name: "gi.contracts/group", + metadata: { + validate: objectOf({ + createdDate: string, + username: string, + identityContractID: string + }), + create() { + const { username, identityContractID } = module_default("state/vuex/state").loggedIn; + return { + createdDate: new Date().toISOString(), + username, + identityContractID + }; + } + }, + getters: { + currentGroupState(state) { + return state; + }, + groupSettings(state, getters) { + return getters.currentGroupState.settings || {}; + }, + groupProfile(state, getters) { + return (username) => { + const profiles = getters.currentGroupState.profiles; + return profiles && profiles[username]; + }; + }, + groupProfiles(state, getters) { + const profiles = {}; + for (const username in getters.currentGroupState.profiles || {}) { + const profile = getters.groupProfile(username); + if (profile.status === PROFILE_STATUS.ACTIVE) { + profiles[username] = profile; + } + } + return profiles; + }, + groupMincomeAmount(state, getters) { + return getters.groupSettings.mincomeAmount; + }, + groupMincomeCurrency(state, getters) { + return getters.groupSettings.mincomeCurrency; + }, + periodStampGivenDate(state, getters) { + return (recentDate) => { + if (typeof recentDate !== "string") { + recentDate = recentDate.toISOString(); + } + const { distributionDate, distributionPeriodLength } = getters.groupSettings; + return periodStampGivenDate({ + recentDate, + periodStart: distributionDate, + periodLength: distributionPeriodLength + }); + }; + }, + periodBeforePeriod(state, getters) { + return (periodStamp) => { + const len = getters.groupSettings.distributionPeriodLength; + return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -len)); + }; + }, + periodAfterPeriod(state, getters) { + return (periodStamp) => { + const len = getters.groupSettings.distributionPeriodLength; + return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), len)); + }; + }, + dueDateForPeriod(state, getters) { + return (periodStamp) => { + return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(getters.periodAfterPeriod(periodStamp)), -DAYS_MILLIS)); + }; + }, + paymentTotalFromUserToUser(state, getters) { + return (fromUser, toUser, periodStamp) => { + const payments = getters.currentGroupState.payments; + const periodPayments = getters.groupPeriodPayments; + const { paymentsFrom, mincomeExchangeRate } = periodPayments[periodStamp] || {}; + const total = (((paymentsFrom || {})[fromUser] || {})[toUser] || []).reduce((a, hash) => { + const payment = payments[hash]; + let { amount, exchangeRate, status } = payment.data; + if (status !== PAYMENT_COMPLETED) { + return a; + } + const paymentCreatedPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate); + if (periodStamp !== paymentCreatedPeriodStamp) { + if (paymentCreatedPeriodStamp !== getters.periodBeforePeriod(periodStamp)) { + console.warn(`paymentTotalFromUserToUser: super old payment shouldn't exist, ignoring! (curPeriod=${periodStamp})`, JSON.stringify(payment)); + return a; + } + exchangeRate *= periodPayments[paymentCreatedPeriodStamp].mincomeExchangeRate; + } + return a + amount * exchangeRate * mincomeExchangeRate; + }, 0); + return saferFloat(total); + }; + }, + paymentHashesForPeriod(state, getters) { + return (periodStamp) => { + const periodPayments = getters.groupPeriodPayments[periodStamp]; + if (periodPayments) { + let hashes = []; + const { paymentsFrom } = periodPayments; + for (const fromUser in paymentsFrom) { + for (const toUser in paymentsFrom[fromUser]) { + hashes = hashes.concat(paymentsFrom[fromUser][toUser]); + } + } + return hashes; + } + }; + }, + groupMembersByUsername(state, getters) { + return Object.keys(getters.groupProfiles); + }, + groupMembersCount(state, getters) { + return getters.groupMembersByUsername.length; + }, + groupMembersPending(state, getters) { + const invites = getters.currentGroupState.invites; + const pendingMembers = {}; + for (const inviteId in invites) { + const invite = invites[inviteId]; + if (invite.status === INVITE_STATUS.VALID && invite.creator !== INVITE_INITIAL_CREATOR) { + pendingMembers[invites[inviteId].invitee] = { + invitedBy: invites[inviteId].creator, + expires: invite.expires + }; + } + } + return pendingMembers; + }, + groupShouldPropose(state, getters) { + return getters.groupMembersCount >= 3; + }, + groupProposalSettings(state, getters) { + return (proposalType2 = PROPOSAL_GENERIC) => { + return getters.groupSettings.proposals[proposalType2]; + }; + }, + groupCurrency(state, getters) { + const mincomeCurrency = getters.groupMincomeCurrency; + return mincomeCurrency && currencies_default[mincomeCurrency]; + }, + groupMincomeFormatted(state, getters) { + return getters.withGroupCurrency?.(getters.groupMincomeAmount); + }, + groupMincomeSymbolWithCode(state, getters) { + return getters.groupCurrency?.symbolWithCode; + }, + groupPeriodPayments(state, getters) { + return getters.currentGroupState.paymentsByPeriod || {}; + }, + withGroupCurrency(state, getters) { + return getters.groupCurrency?.displayWithCurrency; + }, + getChatRooms(state, getters) { + return getters.currentGroupState.chatRooms; + }, + generalChatRoomId(state, getters) { + return getters.currentGroupState.generalChatRoomId; + }, + haveNeedsForThisPeriod(state, getters) { + return (currentPeriod) => { + const groupProfiles = getters.groupProfiles; + const haveNeeds = []; + for (const username in groupProfiles) { + const { incomeDetailsType, joinedDate } = groupProfiles[username]; + if (incomeDetailsType) { + const amount = groupProfiles[username][incomeDetailsType]; + const haveNeed = incomeDetailsType === "incomeAmount" ? amount - getters.groupMincomeAmount : amount; + let when = dateFromPeriodStamp(currentPeriod).toISOString(); + if (dateIsWithinPeriod({ + date: joinedDate, + periodStart: currentPeriod, + periodLength: getters.groupSettings.distributionPeriodLength + })) { + when = joinedDate; + } + haveNeeds.push({ name: username, haveNeed, when }); + } + } + return haveNeeds; + }; + }, + paymentsForPeriod(state, getters) { + return (periodStamp) => { + const hashes = getters.paymentHashesForPeriod(periodStamp); + const events = []; + if (hashes && hashes.length > 0) { + const payments = getters.currentGroupState.payments; + for (const paymentHash of hashes) { + const payment = payments[paymentHash]; + if (payment.data.status === PAYMENT_COMPLETED) { + events.push({ + from: payment.meta.username, + to: payment.data.toUser, + hash: paymentHash, + amount: payment.data.amount, + isLate: !!payment.data.isLate, + when: payment.data.completedDate + }); + } + } + } + return events; + }; + } + }, + actions: { + "gi.contracts/group": { + validate: objectMaybeOf({ + invites: mapOf(string, inviteType), + settings: objectMaybeOf({ + groupName: string, + groupPicture: string, + sharedValues: string, + mincomeAmount: number, + mincomeCurrency: string, + distributionDate: isPeriodStamp, + distributionPeriodLength: number, + minimizeDistribution: boolean, + proposals: objectOf({ + [PROPOSAL_INVITE_MEMBER]: proposalSettingsType, + [PROPOSAL_REMOVE_MEMBER]: proposalSettingsType, + [PROPOSAL_GROUP_SETTING_CHANGE]: proposalSettingsType, + [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposalSettingsType, + [PROPOSAL_GENERIC]: proposalSettingsType + }) + }) + }), + process({ data, meta }, { state, getters }) { + const initialState = merge({ + payments: {}, + paymentsByPeriod: {}, + invites: {}, + proposals: {}, + settings: { + groupCreator: meta.username, + distributionPeriodLength: 30 * DAYS_MILLIS, + inviteExpiryOnboarding: INVITE_EXPIRES_IN_DAYS.ON_BOARDING, + inviteExpiryProposal: INVITE_EXPIRES_IN_DAYS.PROPOSAL + }, + profiles: { + [meta.username]: initGroupProfile(meta.identityContractID, meta.createdDate) + }, + chatRooms: {} + }, data); + for (const key in initialState) { + default2.set(state, key, initialState[key]); + } + initFetchPeriodPayments({ meta, state, getters }); + } + }, + "gi.contracts/group/payment": { + validate: objectMaybeOf({ + toUser: string, + amount: number, + currencyFromTo: tupleOf(string, string), + exchangeRate: number, + txid: string, + status: paymentStatusType, + paymentType, + details: optional(object), + memo: optional(string) + }), + process({ data, meta, hash }, { state, getters }) { + if (data.status === PAYMENT_COMPLETED) { + console.error(`payment: payment ${hash} cannot have status = 'completed'!`, { data, meta, hash }); + throw new errors_exports.GIErrorIgnoreAndBan("payments cannot be instantly completed!"); + } + default2.set(state.payments, hash, { + data: { + ...data, + groupMincome: getters.groupMincomeAmount + }, + meta, + history: [[meta.createdDate, hash]] + }); + const { paymentsFrom } = initFetchPeriodPayments({ meta, state, getters }); + const fromUser = vueFetchInitKV(paymentsFrom, meta.username, {}); + const toUser = vueFetchInitKV(fromUser, data.toUser, []); + toUser.push(hash); + } + }, + "gi.contracts/group/paymentUpdate": { + validate: objectMaybeOf({ + paymentHash: string, + updatedProperties: objectMaybeOf({ + status: paymentStatusType, + details: object, + memo: string + }) + }), + process({ data, meta, hash }, { state, getters }) { + const payment = state.payments[data.paymentHash]; + if (!payment) { + console.error(`paymentUpdate: no payment ${data.paymentHash}`, { data, meta, hash }); + throw new errors_exports.GIErrorIgnoreAndBan("paymentUpdate without existing payment"); + } + if (meta.username !== payment.meta.username && meta.username !== payment.data.toUser) { + console.error(`paymentUpdate: bad username ${meta.username} != ${payment.meta.username} != ${payment.data.username}`, { data, meta, hash }); + throw new errors_exports.GIErrorIgnoreAndBan("paymentUpdate from bad user!"); + } + payment.history.push([meta.createdDate, hash]); + merge(payment.data, data.updatedProperties); + if (data.updatedProperties.status === PAYMENT_COMPLETED) { + payment.data.completedDate = meta.createdDate; + const updatePeriodStamp = getters.periodStampGivenDate(meta.createdDate); + const paymentPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate); + if (comparePeriodStamps(updatePeriodStamp, paymentPeriodStamp) > 0) { + updateAdjustedDistribution({ period: paymentPeriodStamp, getters }); + } else { + updateCurrentDistribution({ meta, state, getters }); + } + } + } + }, + "gi.contracts/group/proposal": { + validate: (data, { state, meta }) => { + objectOf({ + proposalType, + proposalData: object, + votingRule: ruleType, + expires_date_ms: number + })(data); + const dataToCompare = omit(data.proposalData, ["reason"]); + for (const hash in state.proposals) { + const prop = state.proposals[hash]; + if (prop.status !== STATUS_OPEN || prop.data.proposalType !== data.proposalType) { + continue; + } + if (deepEqualJSONType(omit(prop.data.proposalData, ["reason"]), dataToCompare)) { + throw new TypeError(L("There is an identical open proposal.")); + } + } + }, + process({ data, meta, hash }, { state }) { + default2.set(state.proposals, hash, { + data, + meta, + votes: { [meta.username]: VOTE_FOR }, + status: STATUS_OPEN, + payload: null + }); + }, + sideEffect({ contractID, meta, data }, { getters }) { + const { loggedIn } = module_default("state/vuex/state"); + const typeToSubTypeMap = { + [PROPOSAL_INVITE_MEMBER]: "ADD_MEMBER", + [PROPOSAL_REMOVE_MEMBER]: "REMOVE_MEMBER", + [PROPOSAL_GROUP_SETTING_CHANGE]: "CHANGE_MINCOME", + [PROPOSAL_PROPOSAL_SETTING_CHANGE]: "CHANGE_VOTING_RULE", + [PROPOSAL_GENERIC]: "GENERIC" + }; + const myProfile = getters.groupProfile(loggedIn.username); + if (isActionYoungerThanUser(meta, myProfile)) { + module_default("gi.notifications/emit", "NEW_PROPOSAL", { + groupID: contractID, + creator: meta.username, + subtype: typeToSubTypeMap[data.proposalType] + }); + } + } + }, + "gi.contracts/group/proposalVote": { + validate: objectOf({ + proposalHash: string, + vote: string, + passPayload: optional(unionOf(object, string)) + }), + process(message, { state }) { + const { data, hash, meta } = message; + const proposal = state.proposals[data.proposalHash]; + if (!proposal) { + console.error(`proposalVote: no proposal for ${data.proposalHash}!`, { data, meta, hash }); + throw new errors_exports.GIErrorIgnoreAndBan("proposalVote without existing proposal"); + } + default2.set(proposal.votes, meta.username, data.vote); + if (new Date(meta.createdDate).getTime() > proposal.data.expires_date_ms) { + console.warn("proposalVote: vote on expired proposal!", { proposal, data, meta }); + return; + } + const result = rules_default[proposal.data.votingRule](state, proposal.data.proposalType, proposal.votes); + if (result === VOTE_FOR || result === VOTE_AGAINST) { + proposals_default[proposal.data.proposalType][result](state, message); + default2.set(proposal, "dateClosed", meta.createdDate); + } + }, + sideEffect({ contractID, data, meta }, { state, getters }) { + const proposal = state.proposals[data.proposalHash]; + const { loggedIn } = module_default("state/vuex/state"); + const myProfile = getters.groupProfile(loggedIn.username); + if (proposal?.dateClosed && isActionYoungerThanUser(meta, myProfile)) { + module_default("gi.notifications/emit", "PROPOSAL_CLOSED", { + groupID: contractID, + creator: meta.username, + proposalStatus: proposal.status + }); + } + } + }, + "gi.contracts/group/proposalCancel": { + validate: objectOf({ + proposalHash: string + }), + process({ data, meta, contractID }, { state }) { + const proposal = state.proposals[data.proposalHash]; + if (!proposal) { + console.error(`proposalCancel: no proposal for ${data.proposalHash}!`, { data, meta }); + throw new errors_exports.GIErrorIgnoreAndBan("proposalVote without existing proposal"); + } else if (proposal.meta.username !== meta.username) { + console.error(`proposalCancel: proposal ${data.proposalHash} belongs to ${proposal.meta.username} not ${meta.username}!`, { data, meta }); + throw new errors_exports.GIErrorIgnoreAndBan("proposalWithdraw for wrong user!"); + } + default2.set(proposal, "status", STATUS_CANCELLED); + archiveProposal({ state, proposalHash: data.proposalHash, proposal, contractID }); + } + }, + "gi.contracts/group/removeMember": { + validate: (data, { state, getters, meta }) => { + objectOf({ + member: string, + reason: optional(string), + proposalHash: optional(string), + proposalPayload: optional(objectOf({ + secret: string + })) + })(data); + const memberToRemove = data.member; + const membersCount = getters.groupMembersCount; + if (!state.profiles[memberToRemove]) { + throw new TypeError(L("Not part of the group.")); + } + if (membersCount === 1 || memberToRemove === meta.username) { + throw new TypeError(L("Cannot remove yourself.")); + } + if (membersCount < 3) { + if (meta.username !== state.settings.groupCreator) { + throw new TypeError(L("Only the group creator can remove members.")); + } + } else { + const proposal = state.proposals[data.proposalHash]; + if (!proposal) { + throw new TypeError(L("Admin credentials needed and not implemented yet.")); + } + if (!proposal.payload || proposal.payload.secret !== data.proposalPayload.secret) { + throw new TypeError(L("Invalid associated proposal.")); + } + } + }, + process({ data, meta }, { state, getters }) { + memberLeaves({ username: data.member, dateLeft: meta.createdDate }, { meta, state, getters }); + }, + sideEffect({ data, meta, contractID }, { state, getters }) { + const rootState = module_default("state/vuex/state"); + const contracts = rootState.contracts || {}; + const { username } = rootState.loggedIn; + if (data.member === username) { + if (module_default("okTurtles.data/get", "JOINING_GROUP")) { + return; + } + const groupIdToSwitch = Object.keys(contracts).find((cID) => contracts[cID].type === "gi.contracts/group" && cID !== contractID && rootState[cID].settings) || null; + module_default("state/vuex/commit", "setCurrentChatRoomId", {}); + module_default("state/vuex/commit", "setCurrentGroupId", groupIdToSwitch); + module_default("chelonia/contract/remove", contractID).catch((e) => { + console.error(`sideEffect(removeMember): ${e.name} thrown by /remove ${contractID}:`, e); + }); + module_default("chelonia/queueInvocation", contractID, ["gi.actions/identity/saveOurLoginState"]).then(function() { + const router = module_default("controller/router"); + const switchFrom = router.currentRoute.path; + const switchTo = groupIdToSwitch ? "/dashboard" : "/"; + if (switchFrom !== "/join" && switchFrom !== switchTo) { + router.push({ path: switchTo }).catch(console.warn); + } + }).catch((e) => { + console.error(`sideEffect(removeMember): ${e.name} thrown during queueEvent to ${contractID} by saveOurLoginState:`, e); + }); + } else { + const myProfile = getters.groupProfile(username); + if (isActionYoungerThanUser(meta, myProfile)) { + const memberRemovedThemselves = data.member === meta.username; + module_default("gi.notifications/emit", memberRemovedThemselves ? "MEMBER_LEFT" : "MEMBER_REMOVED", { + groupID: contractID, + username: memberRemovedThemselves ? meta.username : data.member + }); + } + } + } + }, + "gi.contracts/group/removeOurselves": { + validate: objectMaybeOf({ + reason: string + }), + process({ data, meta, contractID }, { state, getters }) { + memberLeaves({ username: meta.username, dateLeft: meta.createdDate }, { meta, state, getters }); + module_default("gi.contracts/group/pushSideEffect", contractID, ["gi.contracts/group/removeMember/sideEffect", { + meta, + data: { member: meta.username, reason: data.reason || "" }, + contractID + }]); + } + }, + "gi.contracts/group/invite": { + validate: inviteType, + process({ data, meta }, { state }) { + default2.set(state.invites, data.inviteSecret, data); + } + }, + "gi.contracts/group/inviteAccept": { + validate: objectOf({ + inviteSecret: string + }), + process({ data, meta }, { state }) { + console.debug("inviteAccept:", data, state.invites); + const invite = state.invites[data.inviteSecret]; + if (invite.status !== INVITE_STATUS.VALID) { + console.error(`inviteAccept: invite for ${meta.username} is: ${invite.status}`); + return; + } + default2.set(invite.responses, meta.username, true); + if (Object.keys(invite.responses).length === invite.quantity) { + invite.status = INVITE_STATUS.USED; + } + default2.set(state.profiles, meta.username, initGroupProfile(meta.identityContractID, meta.createdDate)); + }, + async sideEffect({ meta, contractID }, { state }) { + const { loggedIn } = module_default("state/vuex/state"); + const { profiles = {} } = state; + if (meta.username === loggedIn.username) { + for (const name in profiles) { + if (name !== loggedIn.username) { + await module_default("chelonia/contract/sync", profiles[name].contractID); + } + } + } else { + const myProfile = profiles[loggedIn.username]; + await module_default("chelonia/contract/sync", meta.identityContractID); + if (isActionYoungerThanUser(meta, myProfile)) { + module_default("gi.notifications/emit", "MEMBER_ADDED", { + groupID: contractID, + username: meta.username + }); + } + } + } + }, + "gi.contracts/group/inviteRevoke": { + validate: (data, { state, meta }) => { + objectOf({ + inviteSecret: string + })(data); + if (!state.invites[data.inviteSecret]) { + throw new TypeError(L("The link does not exist.")); + } + }, + process({ data, meta }, { state }) { + const invite = state.invites[data.inviteSecret]; + default2.set(invite, "status", INVITE_STATUS.REVOKED); + } + }, + "gi.contracts/group/updateSettings": { + validate: objectMaybeOf({ + groupName: (x) => typeof x === "string", + groupPicture: (x) => typeof x === "string", + sharedValues: (x) => typeof x === "string", + mincomeAmount: (x) => typeof x === "number" && x > 0, + mincomeCurrency: (x) => typeof x === "string" + }), + process({ meta, data }, { state }) { + for (const key in data) { + default2.set(state.settings, key, data[key]); + } + } + }, + "gi.contracts/group/groupProfileUpdate": { + validate: objectMaybeOf({ + incomeDetailsType: (x) => ["incomeAmount", "pledgeAmount"].includes(x), + incomeAmount: (x) => typeof x === "number" && x >= 0, + pledgeAmount: (x) => typeof x === "number" && x >= 0, + nonMonetaryAdd: string, + nonMonetaryEdit: objectOf({ + replace: string, + with: string + }), + nonMonetaryRemove: string, + paymentMethods: arrayOf(objectOf({ + name: string, + value: string + })) + }), + process({ data, meta }, { state, getters }) { + const groupProfile = state.profiles[meta.username]; + const nonMonetary = groupProfile.nonMonetaryContributions; + for (const key in data) { + const value = data[key]; + switch (key) { + case "nonMonetaryAdd": + nonMonetary.push(value); + break; + case "nonMonetaryRemove": + nonMonetary.splice(nonMonetary.indexOf(value), 1); + break; + case "nonMonetaryEdit": + nonMonetary.splice(nonMonetary.indexOf(value.replace), 1, value.with); + break; + default: + default2.set(groupProfile, key, value); + } + } + if (data.incomeDetailsType) { + updateCurrentDistribution({ meta, state, getters }); + } + } + }, + "gi.contracts/group/updateAllVotingRules": { + validate: objectMaybeOf({ + ruleName: (x) => [RULE_PERCENTAGE, RULE_DISAGREEMENT].includes(x), + ruleThreshold: number, + expires_ms: number + }), + process({ data, meta }, { state }) { + if (data.ruleName && data.ruleThreshold) { + for (const proposalSettings in state.settings.proposals) { + default2.set(state.settings.proposals[proposalSettings], "rule", data.ruleName); + default2.set(state.settings.proposals[proposalSettings].ruleSettings[data.ruleName], "threshold", data.ruleThreshold); + } + } + } + }, + "gi.contracts/group/addChatRoom": { + validate: objectOf({ + chatRoomID: string, + attributes: chatRoomAttributesType + }), + process({ data, meta }, { state }) { + const { name, type, privacyLevel } = data.attributes; + default2.set(state.chatRooms, data.chatRoomID, { + creator: meta.username, + name, + type, + privacyLevel, + deletedDate: null, + users: [] + }); + if (!state.generalChatRoomId) { + default2.set(state, "generalChatRoomId", data.chatRoomID); + } + } + }, + "gi.contracts/group/deleteChatRoom": { + validate: (data, { getters, meta }) => { + objectOf({ chatRoomID: string })(data); + if (getters.getChatRooms[data.chatRoomID].creator !== meta.username) { + throw new TypeError(L("Only the channel creator can delete channel.")); + } + }, + process({ data, meta }, { state }) { + default2.delete(state.chatRooms, data.chatRoomID); + } + }, + "gi.contracts/group/leaveChatRoom": { + validate: objectOf({ + chatRoomID: string, + member: string, + leavingGroup: boolean + }), + process({ data, meta }, { state }) { + default2.set(state.chatRooms[data.chatRoomID], "users", state.chatRooms[data.chatRoomID].users.filter((u) => u !== data.member)); + }, + async sideEffect({ meta, data }, { state }) { + const rootState = module_default("state/vuex/state"); + if (meta.username === rootState.loggedIn.username && !module_default("okTurtles.data/get", "JOINING_GROUP")) { + const sendingData = data.leavingGroup ? { member: data.member } : { member: data.member, username: meta.username }; + await module_default("gi.actions/chatroom/leave", { contractID: data.chatRoomID, data: sendingData }); + } + } + }, + "gi.contracts/group/joinChatRoom": { + validate: objectMaybeOf({ + username: string, + chatRoomID: string + }), + process({ data, meta }, { state }) { + const username = data.username || meta.username; + state.chatRooms[data.chatRoomID].users.push(username); + }, + async sideEffect({ meta, data }, { state }) { + const rootState = module_default("state/vuex/state"); + const username = data.username || meta.username; + if (username === rootState.loggedIn.username) { + if (!module_default("okTurtles.data/get", "JOINING_GROUP") || module_default("okTurtles.data/get", "READY_TO_JOIN_CHATROOM")) { + module_default("okTurtles.data/set", "JOINING_CHATROOM_ID", data.chatRoomID); + await module_default("chelonia/contract/sync", data.chatRoomID); + module_default("okTurtles.data/set", "JOINING_CHATROOM_ID", void 0); + module_default("okTurtles.data/set", "READY_TO_JOIN_CHATROOM", false); + } + } + } + }, + "gi.contracts/group/renameChatRoom": { + validate: objectOf({ + chatRoomID: string, + name: string + }), + process({ data, meta }, { state, getters }) { + default2.set(state.chatRooms, data.chatRoomID, { + ...getters.getChatRooms[data.chatRoomID], + name: data.name + }); + } + }, + ...{ + "gi.contracts/group/forceDistributionDate": { + validate: optional, + process({ meta }, { state, getters }) { + getters.groupSettings.distributionDate = dateToPeriodStamp(meta.createdDate); + } + }, + "gi.contracts/group/malformedMutation": { + validate: objectOf({ errorType: string, sideEffect: optional(boolean) }), + process({ data }) { + const ErrorType = errors_exports[data.errorType]; + if (data.sideEffect) + return; + if (ErrorType) { + throw new ErrorType("malformedMutation!"); + } else { + throw new Error(`unknown error type: ${data.errorType}`); + } + }, + sideEffect(message, { state }) { + if (!message.data.sideEffect) + return; + module_default("gi.contracts/group/malformedMutation/process", { + ...message, + data: omit(message.data, ["sideEffect"]) + }, state); + } + } + } + }, + methods: { + "gi.contracts/group/archiveProposal": async function(contractID, proposalHash, proposal) { + const { username } = module_default("state/vuex/state").loggedIn; + const key = `proposals/${username}/${contractID}`; + const proposals2 = await module_default("gi.db/archive/load", key) || []; + proposals2.unshift([proposalHash, proposal]); + while (proposals2.length > MAX_ARCHIVED_PROPOSALS) { + proposals2.pop(); + } + await module_default("gi.db/archive/save", key, proposals2); + module_default("okTurtles.events/emit", PROPOSAL_ARCHIVED, [proposalHash, proposal]); + } + } +}); +//# sourceMappingURL=group.js.map diff --git a/test/contracts/group.js.map b/test/contracts/group.js.map new file mode 100644 index 0000000000..bdee49dc70 --- /dev/null +++ b/test/contracts/group.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/common/errors.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/shared/voting/rules.js", "../../frontend/model/contracts/shared/time.js", "../../frontend/model/contracts/shared/voting/proposals.js", "../../frontend/model/contracts/shared/payments/index.js", "../../frontend/model/contracts/shared/distribution/mincome-proportional.js", "../../frontend/model/contracts/shared/distribution/payments-minimizer.js", "../../frontend/model/contracts/shared/currencies.js", "../../frontend/model/contracts/shared/distribution/distribution.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/group.js"], + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nexport class GIErrorIgnoreAndBan extends Error {\n // ugly boilerplate because JavaScript is stupid\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types\n constructor (...params ) {\n super(...params)\n this.name = 'GIErrorIgnoreAndBan'\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n\n// Used to throw human readable errors on UI.\nexport class GIErrorUIRuntimeError extends Error {\n constructor (...params ) {\n super(...params)\n // this.name = this.constructor.name\n this.name = 'GIErrorUIRuntimeError' // string literal so minifier doesn't overwrite\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n", "'use strict'\n\n \n \n \n \n\nexport default function mincomeProportional (haveNeeds ) {\n let totalHave = 0\n let totalNeed = 0\n const havers = []\n const needers = []\n for (const haveNeed of haveNeeds) {\n if (haveNeed.haveNeed > 0) {\n havers.push(haveNeed)\n totalHave += haveNeed.haveNeed\n } else if (haveNeed.haveNeed < 0) {\n needers.push(haveNeed)\n totalNeed += Math.abs(haveNeed.haveNeed)\n }\n }\n const totalPercent = Math.min(1, totalNeed / totalHave)\n const payments = []\n for (const haver of havers) {\n const distributionAmount = totalPercent * haver.haveNeed\n for (const needer of needers) {\n const belowPercentage = Math.abs(needer.haveNeed) / totalNeed\n payments.push({\n amount: distributionAmount * belowPercentage,\n from: haver.name,\n to: needer.name\n })\n }\n }\n return payments\n}\n", "'use strict'\n\n// greedy algorithm responsible for \"balancing\" payments\n// such that the least number of payments are made.\nexport default function minimizeTotalPaymentsCount (\n distribution \n) {\n const neederTotalReceived = {}\n const haverTotalHave = {}\n const haversSorted = []\n const needersSorted = []\n const minimizedDistribution = []\n for (const todo of distribution) {\n neederTotalReceived[todo.to] = (neederTotalReceived[todo.to] || 0) + todo.amount\n haverTotalHave[todo.from] = (haverTotalHave[todo.from] || 0) + todo.amount\n }\n for (const name in haverTotalHave) {\n haversSorted.push({ name, amount: haverTotalHave[name] })\n }\n for (const name in neederTotalReceived) {\n needersSorted.push({ name, amount: neederTotalReceived[name] })\n }\n // sort haves and needs: greatest to least\n haversSorted.sort((a, b) => b.amount - a.amount)\n needersSorted.sort((a, b) => b.amount - a.amount)\n while (haversSorted.length > 0 && needersSorted.length > 0) {\n const mostHaver = haversSorted.pop()\n const mostNeeder = needersSorted.pop()\n const diff = mostHaver.amount - mostNeeder.amount\n if (diff < 0) {\n // we used up everything the haver had\n minimizedDistribution.push({ amount: mostHaver.amount, from: mostHaver.name, to: mostNeeder.name })\n mostNeeder.amount -= mostHaver.amount\n needersSorted.push(mostNeeder)\n } else if (diff > 0) {\n // we completely filled up the needer's need and still have some left over\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n mostHaver.amount -= mostNeeder.amount\n haversSorted.push(mostHaver)\n } else {\n // a perfect match\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n }\n }\n return minimizedDistribution\n}\n", "'use strict'\n\n \n \n \n \n \n \n \n \n\n// https://github.com/okTurtles/group-income/issues/813#issuecomment-593680834\n// round all accounting to DECIMALS_MAX decimal places max to avoid consensus\n// issues that can arise due to different floating point values\n// at extreme precisions. If this becomes inadequate, instead of increasing\n// this value, switch to a different currency base, e.g. from BTC to mBTC.\nexport const DECIMALS_MAX = 8\n\nfunction commaToDots (value ) {\n // ex: \"1,55\" -> \"1.55\"\n return typeof value === 'string' ? value.replace(/,/, '.') : value.toString()\n}\n\nfunction isNumeric (nr ) {\n return !isNaN((nr ) - parseFloat(nr))\n}\n\nfunction isInDecimalsLimit (nr , decimalsMax ) {\n const decimals = nr.split('.')[1]\n return !decimals || decimals.length <= decimalsMax\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nfunction validateMincome (value , decimalsMax ) {\n const nr = commaToDots(value)\n return isNumeric(nr) && isInDecimalsLimit(nr, decimalsMax)\n}\n\nfunction decimalsOrInt (num , decimalsMax ) {\n // ex: 12.5 -> \"12.50\", but 250 -> \"250\"\n return num.toFixed(decimalsMax).replace(/\\.0+$/, '')\n}\n\nexport function saferFloat (value ) {\n // ex: 1.333333333333333333 -> 1.33333333\n return parseFloat(value.toFixed(DECIMALS_MAX))\n}\n\nexport function normalizeCurrency (value ) {\n // ex: \"1,333333333333333333\" -> 1.33333333\n return saferFloat(parseFloat(commaToDots(value)))\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nexport function mincomePositive (value ) {\n return parseFloat(commaToDots(value)) > 0\n}\n\nfunction makeCurrency (options) {\n const { symbol, symbolWithCode, decimalsMax, formatCurrency } = options\n return {\n symbol,\n symbolWithCode,\n decimalsMax,\n displayWithCurrency: (n ) => formatCurrency(decimalsOrInt(n, decimalsMax)),\n displayWithoutCurrency: (n ) => decimalsOrInt(n, decimalsMax),\n validate: (n ) => validateMincome(n, decimalsMax)\n }\n}\n\n// NOTE: if we needed for some reason, this could also be defined in\n// a json file that's read in and generates this object. For\n// example, that would allow the addition of currencies without\n// having to \"recompile\" a new version of the app.\nconst currencies = {\n USD: makeCurrency({\n symbol: '$',\n symbolWithCode: '$ USD',\n decimalsMax: 2,\n formatCurrency: amount => '$' + amount\n }),\n EUR: makeCurrency({\n symbol: '\u20AC',\n symbolWithCode: '\u20AC EUR',\n decimalsMax: 2,\n formatCurrency: amount => '\u20AC' + amount\n }),\n BTC: makeCurrency({\n symbol: '\u0243',\n symbolWithCode: '\u0243 BTC',\n decimalsMax: DECIMALS_MAX,\n formatCurrency: amount => amount + '\u0243'\n })\n}\n\nexport default currencies\n", "'use strict'\n\nimport mincomeProportional from './mincome-proportional.js'\nimport minimizeTotalPaymentsCount from './payments-minimizer.js'\nimport { cloneDeep } from '../giLodash.js'\nimport { saferFloat, DECIMALS_MAX } from '../currencies.js'\n\n \n\nconst tinyNum = 1 / Math.pow(10, DECIMALS_MAX)\n\nexport function unadjustedDistribution ({ haveNeeds = [], minimize = true } \n \n ) {\n const distribution = mincomeProportional(haveNeeds)\n return minimize ? minimizeTotalPaymentsCount(distribution) : distribution\n}\n\nexport function adjustedDistribution (\n { distribution, payments, dueOn } \n) {\n distribution = cloneDeep(distribution)\n // ensure the total is set because of how reduceDistribution works\n for (const todo of distribution) {\n todo.total = todo.amount\n }\n distribution = subtractDistributions(distribution, payments)\n // remove any todos for containing miniscule amounts\n // and pledgers who switched sides should have their todos removed\n .filter(todo => todo.amount >= tinyNum)\n for (const todo of distribution) {\n todo.amount = saferFloat(todo.amount)\n todo.total = saferFloat(todo.total)\n todo.partial = todo.total !== todo.amount\n todo.isLate = false\n todo.dueOn = dueOn\n }\n // TODO: add in latePayments to the end of the distribution\n // consider passing in latePayments\n return distribution\n}\n\n// Merges multiple payments between any combinations two of users:\nfunction reduceDistribution (payments ) {\n // Don't modify the payments list/object parameter in-place, as this is not intended:\n payments = cloneDeep(payments)\n for (let i = 0; i < payments.length; i++) {\n const paymentA = payments[i]\n for (let j = i + 1; j < payments.length; j++) {\n const paymentB = payments[j]\n\n // Were paymentA and paymentB between the same two users?\n if ((paymentA.from === paymentB.from && paymentA.to === paymentB.to) ||\n (paymentA.to === paymentB.from && paymentA.from === paymentB.to)) {\n // Add or subtract paymentB's amount to paymentA's amount, depending on the relative\n // direction of the two payments:\n paymentA.amount += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.amount\n paymentA.total += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.total\n // Remove paymentB from payments, and decrement the inner sentinal loop variable:\n payments.splice(j, 1)\n j--\n }\n }\n }\n return payments\n}\n\nfunction addDistributions (paymentsA , paymentsB ) {\n return reduceDistribution([...paymentsA, ...paymentsB])\n}\n\nfunction subtractDistributions (paymentsA , paymentsB ) {\n // Don't modify any payment list/objects parameters in-place, as this is not intended:\n paymentsB = cloneDeep(paymentsB)\n // Reverse the sign of the second operand's amounts so that the final addition is actually subtraction:\n for (const p of paymentsB) {\n p.amount *= -1\n p.total *= -1\n }\n return addDistributions(paymentsA, paymentsB)\n}\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, Errors, L } from '@common/common.js'\nimport votingRules, { ruleType, VOTE_FOR, VOTE_AGAINST, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './shared/voting/rules.js'\nimport proposals, { proposalType, proposalSettingsType, archiveProposal } from './shared/voting/proposals.js'\nimport {\n PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC, STATUS_OPEN, STATUS_CANCELLED, MAX_ARCHIVED_PROPOSALS, PROPOSAL_ARCHIVED,\n INVITE_INITIAL_CREATOR, INVITE_STATUS, PROFILE_STATUS, INVITE_EXPIRES_IN_DAYS\n} from './shared/constants.js'\nimport { paymentStatusType, paymentType, PAYMENT_COMPLETED } from './shared/payments/index.js'\nimport { merge, deepEqualJSONType, omit } from './shared/giLodash.js'\nimport { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, periodStampGivenDate, dateIsWithinPeriod, DAYS_MILLIS } from './shared/time.js'\nimport { unadjustedDistribution, adjustedDistribution } from './shared/distribution/distribution.js'\nimport currencies, { saferFloat } from './shared/currencies.js'\nimport { inviteType, chatRoomAttributesType } from './shared/types.js'\nimport { arrayOf, mapOf, objectOf, objectMaybeOf, optional, string, number, boolean, object, unionOf, tupleOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction vueFetchInitKV (obj , key , initialValue ) {\n let value = obj[key]\n if (!value) {\n Vue.set(obj, key, initialValue)\n value = obj[key]\n }\n return value\n}\n\nfunction initGroupProfile (contractID , joinedDate ) {\n return {\n globalUsername: '', // TODO: this? e.g. groupincome:greg / namecoin:bob / ens:alice\n contractID,\n joinedDate,\n nonMonetaryContributions: [],\n status: PROFILE_STATUS.ACTIVE,\n departedDate: null\n }\n}\n\nfunction initPaymentPeriod ({ getters }) {\n return {\n // this saved so that it can be used when creating a new payment\n initialCurrency: getters.groupMincomeCurrency,\n // TODO: should we also save the first period's currency exchange rate..?\n // all payments during the period use this to set their exchangeRate\n // see notes and code in groupIncomeAdjustedDistribution for details.\n // TODO: for the currency change proposal, have it update the mincomeExchangeRate\n // using .mincomeExchangeRate *= proposal.exchangeRate\n mincomeExchangeRate: 1, // modified by proposals to change mincome currency\n paymentsFrom: {}, // fromUser => toUser => Array\n // snapshot of adjusted distribution after each completed payment\n // yes, it is possible a payment began in one period and completed in another\n // in which case lastAdjustedDistribution for the previous period will be updated\n lastAdjustedDistribution: null,\n // snapshot of haveNeeds. made only when there are no payments\n haveNeedsSnapshot: null\n }\n}\n\n// NOTE: do not call any of these helper functions from within a getter b/c they modify state!\n\nfunction clearOldPayments ({ state, getters }) {\n const sortedPeriodKeys = Object.keys(state.paymentsByPeriod).sort()\n // save two periods worth of payments, max\n while (sortedPeriodKeys.length > 2) {\n const period = sortedPeriodKeys.shift()\n for (const paymentHash of getters.paymentHashesForPeriod(period)) {\n Vue.delete(state.payments, paymentHash)\n // TODO: archive the old payments in a sideEffect, not here\n }\n Vue.delete(state.paymentsByPeriod, period)\n }\n}\n\nfunction initFetchPeriodPayments ({ meta, state, getters }) {\n const period = getters.periodStampGivenDate(meta.createdDate)\n const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ getters }))\n clearOldPayments({ state, getters })\n return periodPayments\n}\n\n// this function is called each time a payment is completed or a user adjusts their income details.\n// TODO: call also when mincome is adjusted\nfunction updateCurrentDistribution ({ meta, state, getters }) {\n const curPeriodPayments = initFetchPeriodPayments({ meta, state, getters })\n const period = getters.periodStampGivenDate(meta.createdDate)\n const noPayments = Object.keys(curPeriodPayments.paymentsFrom).length === 0\n // update distributionDate if we've passed into the next period\n if (comparePeriodStamps(period, getters.groupSettings.distributionDate) > 0) {\n getters.groupSettings.distributionDate = period\n }\n // save haveNeeds if there are no payments or the haveNeeds haven't been saved yet\n if (noPayments || !curPeriodPayments.haveNeedsSnapshot) {\n curPeriodPayments.haveNeedsSnapshot = getters.haveNeedsForThisPeriod(period)\n }\n // if there are payments this period, save the adjusted distribution\n if (!noPayments) {\n updateAdjustedDistribution({ period, getters })\n }\n}\n\nfunction updateAdjustedDistribution ({ period, getters }) {\n const payments = getters.groupPeriodPayments[period]\n if (payments && payments.haveNeedsSnapshot) {\n const minimize = getters.groupSettings.minimizeDistribution\n payments.lastAdjustedDistribution = adjustedDistribution({\n distribution: unadjustedDistribution({ haveNeeds: payments.haveNeedsSnapshot, minimize }),\n payments: getters.paymentsForPeriod(period),\n dueOn: getters.dueDateForPeriod(period)\n }).filter(todo => {\n // only return todos for active members\n return getters.groupProfile(todo.to).status === PROFILE_STATUS.ACTIVE\n })\n }\n}\n\nfunction memberLeaves ({ username, dateLeft }, { meta, state, getters }) {\n state.profiles[username].status = PROFILE_STATUS.REMOVED\n state.profiles[username].departedDate = dateLeft\n // remove any todos for this member from the adjusted distribution\n updateCurrentDistribution({ meta, state, getters })\n}\n\nfunction isActionYoungerThanUser (actionMeta , userProfile ) {\n // A util function that checks if an action (or event) in a group occurred after a particular user joined a group.\n // This is used mostly for checking if a notification should be sent for that user or not.\n // e.g.) user-2 who joined a group later than user-1 (who is the creator of the group) doesn't need to receive\n // 'MEMBER_ADDED' notification for user-1.\n // In some situations, userProfile is undefined, for example, when inviteAccept is called in\n // certain situations. So we need to check for that here.\n return Boolean(userProfile) && compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/group',\n metadata: {\n validate: objectOf({\n createdDate: string,\n username: string,\n identityContractID: string\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n // TODO: We may want to get the time from the server instead of relying on\n // the client in case the client's clock isn't set correctly.\n // the only issue here is that it involves an async function...\n // See: https://github.com/okTurtles/group-income/issues/531\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n // These getters are restricted only to the contract's state.\n // Do not access state outside the contract state inside of them.\n // For example, if the getter you use tries to access `state.loggedIn`,\n // that will break the `latestContractState` function in state.js.\n // It is only safe to access state outside of the contract in a contract action's\n // `sideEffect` function (as long as it doesn't modify contract state)\n getters: {\n // we define `currentGroupState` here so that we can redefine it in state.js\n // so that we can re-use these getter definitions in state.js since they are\n // compatible with Vuex getter definitions.\n // Here `state` refers to the individual group contract's state, the equivalent\n // of `vuexRootState[someGroupContractID]`.\n currentGroupState (state) {\n return state\n },\n groupSettings (state, getters) {\n return getters.currentGroupState.settings || {}\n },\n groupProfile (state, getters) {\n return username => {\n const profiles = getters.currentGroupState.profiles\n return profiles && profiles[username]\n }\n },\n groupProfiles (state, getters) {\n const profiles = {}\n for (const username in (getters.currentGroupState.profiles || {})) {\n const profile = getters.groupProfile(username)\n if (profile.status === PROFILE_STATUS.ACTIVE) {\n profiles[username] = profile\n }\n }\n return profiles\n },\n groupMincomeAmount (state, getters) {\n return getters.groupSettings.mincomeAmount\n },\n groupMincomeCurrency (state, getters) {\n return getters.groupSettings.mincomeCurrency\n },\n periodStampGivenDate (state, getters) {\n return (recentDate ) => {\n if (typeof recentDate !== 'string') {\n recentDate = recentDate.toISOString()\n }\n const { distributionDate, distributionPeriodLength } = getters.groupSettings\n return periodStampGivenDate({\n recentDate,\n periodStart: distributionDate,\n periodLength: distributionPeriodLength\n })\n }\n },\n periodBeforePeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -len))\n }\n },\n periodAfterPeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), len))\n }\n },\n dueDateForPeriod (state, getters) {\n return (periodStamp ) => {\n return dateToPeriodStamp(\n addTimeToDate(\n dateFromPeriodStamp(getters.periodAfterPeriod(periodStamp)),\n -DAYS_MILLIS\n )\n )\n }\n },\n paymentTotalFromUserToUser (state, getters) {\n return (fromUser, toUser, periodStamp) => {\n const payments = getters.currentGroupState.payments\n const periodPayments = getters.groupPeriodPayments\n const { paymentsFrom, mincomeExchangeRate } = periodPayments[periodStamp] || {}\n // NOTE: @babel/plugin-proposal-optional-chaining would come in super-handy\n // here, but I couldn't get it to work with our linter. :(\n // https://github.com/babel/babel-eslint/issues/511\n const total = (((paymentsFrom || {})[fromUser] || {})[toUser] || []).reduce((a, hash) => {\n const payment = payments[hash]\n let { amount, exchangeRate, status } = payment.data\n if (status !== PAYMENT_COMPLETED) {\n return a\n }\n const paymentCreatedPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n // if this payment is from a previous period, then make sure to take into account\n // any proposals that passed in between the payment creation and the payment\n // completion that modified the group currency by multiplying both period's\n // exchange rates\n if (periodStamp !== paymentCreatedPeriodStamp) {\n if (paymentCreatedPeriodStamp !== getters.periodBeforePeriod(periodStamp)) {\n console.warn(`paymentTotalFromUserToUser: super old payment shouldn't exist, ignoring! (curPeriod=${periodStamp})`, JSON.stringify(payment))\n return a\n }\n exchangeRate *= periodPayments[paymentCreatedPeriodStamp].mincomeExchangeRate\n }\n return a + (amount * exchangeRate * mincomeExchangeRate)\n }, 0)\n return saferFloat(total)\n }\n },\n paymentHashesForPeriod (state, getters) {\n return (periodStamp) => {\n const periodPayments = getters.groupPeriodPayments[periodStamp]\n if (periodPayments) {\n let hashes = []\n const { paymentsFrom } = periodPayments\n for (const fromUser in paymentsFrom) {\n for (const toUser in paymentsFrom[fromUser]) {\n hashes = hashes.concat(paymentsFrom[fromUser][toUser])\n }\n }\n return hashes\n }\n }\n },\n groupMembersByUsername (state, getters) {\n return Object.keys(getters.groupProfiles)\n },\n groupMembersCount (state, getters) {\n return getters.groupMembersByUsername.length\n },\n groupMembersPending (state, getters) {\n const invites = getters.currentGroupState.invites\n const pendingMembers = {}\n for (const inviteId in invites) {\n const invite = invites[inviteId]\n if (\n invite.status === INVITE_STATUS.VALID &&\n invite.creator !== INVITE_INITIAL_CREATOR\n ) {\n pendingMembers[invites[inviteId].invitee] = {\n invitedBy: invites[inviteId].creator,\n expires: invite.expires\n }\n }\n }\n return pendingMembers\n },\n groupShouldPropose (state, getters) {\n return getters.groupMembersCount >= 3\n },\n groupProposalSettings (state, getters) {\n return (proposalType = PROPOSAL_GENERIC) => {\n return getters.groupSettings.proposals[proposalType]\n }\n },\n groupCurrency (state, getters) {\n const mincomeCurrency = getters.groupMincomeCurrency\n return mincomeCurrency && currencies[mincomeCurrency]\n },\n groupMincomeFormatted (state, getters) {\n return getters.withGroupCurrency?.(getters.groupMincomeAmount)\n },\n groupMincomeSymbolWithCode (state, getters) {\n return getters.groupCurrency?.symbolWithCode\n },\n groupPeriodPayments (state, getters) {\n // note: a lot of code expects this to return an object, so keep the || {} below\n return getters.currentGroupState.paymentsByPeriod || {}\n },\n withGroupCurrency (state, getters) {\n // TODO: If this group has no defined mincome currency, not even a default one like\n // USD, then calling this function is probably an error which should be reported.\n // Just make sure the UI doesn't break if an exception is thrown, since this is\n // bound to the UI in some location.\n return getters.groupCurrency?.displayWithCurrency\n },\n getChatRooms (state, getters) {\n return getters.currentGroupState.chatRooms\n },\n generalChatRoomId (state, getters) {\n return getters.currentGroupState.generalChatRoomId\n },\n // getter is named haveNeedsForThisPeriod instead of haveNeedsForPeriod because it uses\n // getters.groupProfiles - and that is always based on the most recent values. we still\n // pass in the current period because it's used to set the \"when\" property\n haveNeedsForThisPeriod (state, getters) {\n return (currentPeriod ) => {\n // NOTE: if we ever switch back to the \"real-time\" adjusted distribution algorithm,\n // make sure that this function also handles userExitsGroupEvent\n const groupProfiles = getters.groupProfiles // TODO: these should use the haveNeeds for the specific period's distribution period\n const haveNeeds = []\n for (const username in groupProfiles) {\n const { incomeDetailsType, joinedDate } = groupProfiles[username]\n if (incomeDetailsType) {\n const amount = groupProfiles[username][incomeDetailsType]\n const haveNeed = incomeDetailsType === 'incomeAmount' ? amount - getters.groupMincomeAmount : amount\n // construct 'when' this way in case we ever use a pro-rated algorithm\n let when = dateFromPeriodStamp(currentPeriod).toISOString()\n if (dateIsWithinPeriod({\n date: joinedDate,\n periodStart: currentPeriod,\n periodLength: getters.groupSettings.distributionPeriodLength\n })) {\n when = joinedDate\n }\n haveNeeds.push({ name: username, haveNeed, when })\n }\n }\n return haveNeeds\n }\n },\n paymentsForPeriod (state, getters) {\n return (periodStamp) => {\n const hashes = getters.paymentHashesForPeriod(periodStamp)\n const events = []\n if (hashes && hashes.length > 0) {\n const payments = getters.currentGroupState.payments\n for (const paymentHash of hashes) {\n const payment = payments[paymentHash]\n if (payment.data.status === PAYMENT_COMPLETED) {\n events.push({\n from: payment.meta.username,\n to: payment.data.toUser,\n hash: paymentHash,\n amount: payment.data.amount,\n isLate: !!payment.data.isLate,\n when: payment.data.completedDate\n })\n }\n }\n }\n return events\n }\n }\n // distributionEventsForMonth (state, getters) {\n // return (monthstamp) => {\n // // NOTE: if we ever switch back to the \"real-time\" adjusted distribution\n // // algorithm, make sure that this function also handles userExitsGroupEvent\n // const distributionEvents = getters.haveNeedEventsForMonth(monthstamp)\n // const paymentEvents = getters.paymentEventsForMonth(monthstamp)\n // distributionEvents.splice(distributionEvents.length, 0, paymentEvents)\n // return distributionEvents.sort((a, b) => compareISOTimestamps(a.data.when, b.data.when))\n // }\n // }\n },\n // NOTE: All mutations must be atomic in their edits of the contract state.\n // THEY ARE NOT to farm out any further mutations through the async actions!\n actions: {\n // this is the constructor\n 'gi.contracts/group': {\n validate: objectMaybeOf({\n invites: mapOf(string, inviteType),\n settings: objectMaybeOf({\n // TODO: add 'groupPubkey'\n groupName: string,\n groupPicture: string,\n sharedValues: string,\n mincomeAmount: number,\n mincomeCurrency: string,\n distributionDate: isPeriodStamp,\n distributionPeriodLength: number,\n minimizeDistribution: boolean,\n proposals: objectOf({\n [PROPOSAL_INVITE_MEMBER]: proposalSettingsType,\n [PROPOSAL_REMOVE_MEMBER]: proposalSettingsType,\n [PROPOSAL_GROUP_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_GENERIC]: proposalSettingsType\n })\n })\n }),\n process ({ data, meta }, { state, getters }) {\n // TODO: checkpointing: https://github.com/okTurtles/group-income/issues/354\n const initialState = merge({\n payments: {},\n paymentsByPeriod: {},\n invites: {},\n proposals: {}, // hashes => {} TODO: this, see related TODOs in GroupProposal\n settings: {\n groupCreator: meta.username,\n distributionPeriodLength: 30 * DAYS_MILLIS,\n inviteExpiryOnboarding: INVITE_EXPIRES_IN_DAYS.ON_BOARDING,\n inviteExpiryProposal: INVITE_EXPIRES_IN_DAYS.PROPOSAL\n },\n profiles: {\n [meta.username]: initGroupProfile(meta.identityContractID, meta.createdDate)\n },\n chatRooms: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n initFetchPeriodPayments({ meta, state, getters })\n }\n },\n 'gi.contracts/group/payment': {\n validate: objectMaybeOf({\n // TODO: how to handle donations to okTurtles?\n // TODO: how to handle payments to groups or users outside of this group?\n toUser: string,\n amount: number,\n currencyFromTo: tupleOf(string, string), // must be one of the keys in currencies.js (e.g. USD, EUR, etc.) TODO: handle old clients not having one of these keys, see OP_PROTOCOL_UPGRADE https://github.com/okTurtles/group-income/issues/603\n // multiply 'amount' by 'exchangeRate', which must always be\n // based on the initialCurrency of the period in which this payment was created.\n // it is then further multiplied by the period's 'mincomeExchangeRate', which\n // is modified if any proposals pass to change the mincomeCurrency\n exchangeRate: number,\n txid: string,\n status: paymentStatusType,\n paymentType: paymentType,\n details: optional(object),\n memo: optional(string)\n }),\n process ({ data, meta, hash }, { state, getters }) {\n if (data.status === PAYMENT_COMPLETED) {\n console.error(`payment: payment ${hash} cannot have status = 'completed'!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('payments cannot be instantly completed!')\n }\n Vue.set(state.payments, hash, {\n data: {\n ...data,\n groupMincome: getters.groupMincomeAmount\n },\n meta,\n history: [[meta.createdDate, hash]]\n })\n const { paymentsFrom } = initFetchPeriodPayments({ meta, state, getters })\n const fromUser = vueFetchInitKV(paymentsFrom, meta.username, {})\n const toUser = vueFetchInitKV(fromUser, data.toUser, [])\n toUser.push(hash)\n // TODO: handle completed payments here too! (for manual payment support)\n }\n },\n 'gi.contracts/group/paymentUpdate': {\n validate: objectMaybeOf({\n paymentHash: string,\n updatedProperties: objectMaybeOf({\n status: paymentStatusType,\n details: object,\n memo: string\n })\n }),\n process ({ data, meta, hash }, { state, getters }) {\n // TODO: we don't want to keep a history of all payments in memory all the time\n // https://github.com/okTurtles/group-income/issues/426\n const payment = state.payments[data.paymentHash]\n // TODO: move these types of validation errors into the validate function so\n // that they can be done before sending as well as upon receiving\n if (!payment) {\n console.error(`paymentUpdate: no payment ${data.paymentHash}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate without existing payment')\n }\n // if the payment is being modified by someone other than the person who sent or received it, throw an exception\n if (meta.username !== payment.meta.username && meta.username !== payment.data.toUser) {\n console.error(`paymentUpdate: bad username ${meta.username} != ${payment.meta.username} != ${payment.data.username}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate from bad user!')\n }\n payment.history.push([meta.createdDate, hash])\n merge(payment.data, data.updatedProperties)\n // we update \"this period\"'s snapshot 'lastAdjustedDistribution' on each completed payment\n if (data.updatedProperties.status === PAYMENT_COMPLETED) {\n payment.data.completedDate = meta.createdDate\n // update the current distribution unless this update is for a payment from the previous period\n const updatePeriodStamp = getters.periodStampGivenDate(meta.createdDate)\n const paymentPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n if (comparePeriodStamps(updatePeriodStamp, paymentPeriodStamp) > 0) {\n updateAdjustedDistribution({ period: paymentPeriodStamp, getters })\n } else {\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n }\n },\n 'gi.contracts/group/proposal': {\n validate: (data, { state, meta }) => {\n objectOf({\n proposalType: proposalType,\n proposalData: object, // data for Vue widgets\n votingRule: ruleType,\n expires_date_ms: number // calculate by grabbing proposal expiry from group properties and add to `meta.createdDate`\n })(data)\n\n const dataToCompare = omit(data.proposalData, ['reason'])\n\n // Validate this isn't a duplicate proposal\n for (const hash in state.proposals) {\n const prop = state.proposals[hash]\n if (prop.status !== STATUS_OPEN || prop.data.proposalType !== data.proposalType) {\n continue\n }\n\n if (deepEqualJSONType(omit(prop.data.proposalData, ['reason']), dataToCompare)) {\n throw new TypeError(L('There is an identical open proposal.'))\n }\n\n // TODO - verify if type of proposal already exists (SETTING_CHANGE).\n }\n },\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.proposals, hash, {\n data,\n meta,\n votes: { [meta.username]: VOTE_FOR },\n status: STATUS_OPEN,\n payload: null // set later by group/proposalVote\n })\n // TODO: save all proposals disk so that we only keep open proposals in memory\n // TODO: create a global timer to auto-pass/archive expired votes\n // make sure to set that proposal's status as STATUS_EXPIRED if it's expired\n },\n sideEffect ({ contractID, meta, data }, { getters }) {\n const { loggedIn } = sbp('state/vuex/state')\n const typeToSubTypeMap = {\n [PROPOSAL_INVITE_MEMBER]: 'ADD_MEMBER',\n [PROPOSAL_REMOVE_MEMBER]: 'REMOVE_MEMBER',\n [PROPOSAL_GROUP_SETTING_CHANGE]: 'CHANGE_MINCOME',\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: 'CHANGE_VOTING_RULE',\n [PROPOSAL_GENERIC]: 'GENERIC'\n }\n\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'NEW_PROPOSAL', {\n groupID: contractID,\n creator: meta.username,\n subtype: typeToSubTypeMap[data.proposalType]\n })\n }\n }\n },\n 'gi.contracts/group/proposalVote': {\n validate: objectOf({\n proposalHash: string,\n vote: string,\n passPayload: optional(unionOf(object, string)) // TODO: this, somehow we need to send an OP_KEY_ADD GIMessage to add a generated once-only writeonly message public key to the contract, and (encrypted) include the corresponding invite link, also, we need all clients to verify that this message/operation was valid to prevent a hacked client from adding arbitrary OP_KEY_ADD messages, and automatically ban anyone generating such messages\n }),\n process (message, { state }) {\n const { data, hash, meta } = message\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalVote: no proposal for ${data.proposalHash}!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n }\n Vue.set(proposal.votes, meta.username, data.vote)\n // TODO: handle vote pass/fail\n // check if proposal is expired, if so, ignore (but log vote)\n if (new Date(meta.createdDate).getTime() > proposal.data.expires_date_ms) {\n console.warn('proposalVote: vote on expired proposal!', { proposal, data, meta })\n // TODO: display warning or something\n return\n }\n // see if this is a deciding vote\n const result = votingRules[proposal.data.votingRule](state, proposal.data.proposalType, proposal.votes)\n if (result === VOTE_FOR || result === VOTE_AGAINST) {\n // handles proposal pass or fail, will update proposal.status accordingly\n proposals[proposal.data.proposalType][result](state, message)\n Vue.set(proposal, 'dateClosed', meta.createdDate)\n }\n },\n sideEffect ({ contractID, data, meta }, { state, getters }) {\n const proposal = state.proposals[data.proposalHash]\n const { loggedIn } = sbp('state/vuex/state')\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (proposal?.dateClosed &&\n isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'PROPOSAL_CLOSED', {\n groupID: contractID,\n creator: meta.username,\n proposalStatus: proposal.status\n })\n }\n }\n },\n 'gi.contracts/group/proposalCancel': {\n validate: objectOf({\n proposalHash: string\n }),\n process ({ data, meta, contractID }, { state }) {\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalCancel: no proposal for ${data.proposalHash}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n } else if (proposal.meta.username !== meta.username) {\n console.error(`proposalCancel: proposal ${data.proposalHash} belongs to ${proposal.meta.username} not ${meta.username}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalWithdraw for wrong user!')\n }\n Vue.set(proposal, 'status', STATUS_CANCELLED)\n archiveProposal({ state, proposalHash: data.proposalHash, proposal, contractID })\n }\n },\n 'gi.contracts/group/removeMember': {\n validate: (data, { state, getters, meta }) => {\n objectOf({\n member: string, // username to remove\n reason: optional(string),\n // In case it happens in a big group (by proposal)\n // we need to validate the associated proposal.\n proposalHash: optional(string),\n proposalPayload: optional(objectOf({\n secret: string // NOTE: simulate the OP_KEY_* stuff for now\n }))\n })(data)\n\n const memberToRemove = data.member\n const membersCount = getters.groupMembersCount\n\n if (!state.profiles[memberToRemove]) {\n throw new TypeError(L('Not part of the group.'))\n }\n if (membersCount === 1 || memberToRemove === meta.username) {\n throw new TypeError(L('Cannot remove yourself.'))\n }\n\n if (membersCount < 3) {\n // In a small group only the creator can remove someone\n // TODO: check whether meta.username has required admin permissions\n if (meta.username !== state.settings.groupCreator) {\n throw new TypeError(L('Only the group creator can remove members.'))\n }\n } else {\n // In a big group a removal can only happen through a proposal\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // TODO this\n throw new TypeError(L('Admin credentials needed and not implemented yet.'))\n }\n\n if (!proposal.payload || proposal.payload.secret !== data.proposalPayload.secret) {\n throw new TypeError(L('Invalid associated proposal.'))\n }\n }\n },\n process ({ data, meta }, { state, getters }) {\n memberLeaves(\n { username: data.member, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n },\n sideEffect ({ data, meta, contractID }, { state, getters }) {\n const rootState = sbp('state/vuex/state')\n const contracts = rootState.contracts || {}\n const { username } = rootState.loggedIn\n\n if (data.member === username) {\n // If this member is re-joining the group, ignore the rest\n // so the member doesn't remove themself again.\n if (sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n return\n }\n\n const groupIdToSwitch = Object.keys(contracts)\n .find(cID => contracts[cID].type === 'gi.contracts/group' &&\n cID !== contractID && rootState[cID].settings) || null\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {})\n sbp('state/vuex/commit', 'setCurrentGroupId', groupIdToSwitch)\n // we can't await on this in here, because it will cause a deadlock, since Chelonia processes\n // this sideEffect on the eventqueue for this contractID, and /remove uses that same eventqueue\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown by /remove ${contractID}:`, e)\n })\n // this looks crazy, but doing this was necessary to fix a race condition in the\n // group-member-removal Cypress tests where due to the ordering of asynchronous events\n // we were getting the same latestHash upon re-logging in for test \"user2 rejoins groupA\".\n // We add it to the same queue as '/remove' above gets run on so that it is run after\n // contractID is removed. See also comments in 'gi.actions/identity/login'.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/saveOurLoginState'])\n .then(function () {\n const router = sbp('controller/router')\n const switchFrom = router.currentRoute.path\n const switchTo = groupIdToSwitch ? '/dashboard' : '/'\n if (switchFrom !== '/join' && switchFrom !== switchTo) {\n router.push({ path: switchTo }).catch(console.warn)\n }\n }).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown during queueEvent to ${contractID} by saveOurLoginState:`, e)\n })\n // TODO - #828 remove other group members contracts if applicable\n } else {\n const myProfile = getters.groupProfile(username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n const memberRemovedThemselves = data.member === meta.username\n\n sbp('gi.notifications/emit', // emit a notification for a member removal.\n memberRemovedThemselves ? 'MEMBER_LEFT' : 'MEMBER_REMOVED',\n {\n groupID: contractID,\n username: memberRemovedThemselves ? meta.username : data.member\n })\n }\n // TODO - #828 remove the member contract if applicable.\n // problem is, if they're in another group we're also a part of, or if we\n // have a DM with them, we don't want to do this. may need to use manual reference counting\n // sbp('chelonia/contract/release', getters.groupProfile(data.member).contractID)\n }\n // TODO - #850 verify open proposals and see if they need some re-adjustment.\n }\n },\n 'gi.contracts/group/removeOurselves': {\n validate: objectMaybeOf({\n reason: string\n }),\n process ({ data, meta, contractID }, { state, getters }) {\n memberLeaves(\n { username: meta.username, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', {\n meta,\n data: { member: meta.username, reason: data.reason || '' },\n contractID\n }]\n )\n }\n },\n 'gi.contracts/group/invite': {\n validate: inviteType,\n process ({ data, meta }, { state }) {\n Vue.set(state.invites, data.inviteSecret, data)\n }\n },\n 'gi.contracts/group/inviteAccept': {\n validate: objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n }),\n process ({ data, meta }, { state }) {\n console.debug('inviteAccept:', data, state.invites)\n const invite = state.invites[data.inviteSecret]\n if (invite.status !== INVITE_STATUS.VALID) {\n console.error(`inviteAccept: invite for ${meta.username} is: ${invite.status}`)\n return\n }\n Vue.set(invite.responses, meta.username, true)\n if (Object.keys(invite.responses).length === invite.quantity) {\n invite.status = INVITE_STATUS.USED\n }\n // TODO: ensure `meta.username` is unique for the lifetime of the username\n // since we are making it possible for the same username to leave and\n // rejoin the group. All of their past posts will be re-associated with\n // them upon re-joining.\n Vue.set(state.profiles, meta.username, initGroupProfile(meta.identityContractID, meta.createdDate))\n // If we're triggered by handleEvent in state.js (and not latestContractState)\n // then the asynchronous sideEffect function will get called next\n // and we will subscribe to this new user's identity contract\n },\n // !! IMPORANT!!\n // Actions here MUST NOT modify contract state!\n // They MUST NOT call 'commit'!\n // They should only coordinate the actions of outside contracts.\n // Otherwise `latestContractState` and `handleEvent` will not produce same state!\n async sideEffect ({ meta, contractID }, { state }) {\n const { loggedIn } = sbp('state/vuex/state')\n const { profiles = {} } = state\n\n // TODO: per #257 this will ,have to be encompassed in a recoverable transaction\n // however per #610 that might be handled in handleEvent (?), or per #356 might not be needed\n if (meta.username === loggedIn.username) {\n // we're the person who just accepted the group invite\n // so subscribe to founder's IdentityContract & everyone else's\n for (const name in profiles) {\n if (name !== loggedIn.username) {\n await sbp('chelonia/contract/sync', profiles[name].contractID)\n }\n }\n } else {\n const myProfile = profiles[loggedIn.username]\n // we're an existing member of the group getting notified that a\n // new member has joined, so subscribe to their identity contract\n await sbp('chelonia/contract/sync', meta.identityContractID)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'MEMBER_ADDED', { // emit a notification for a member addition.\n groupID: contractID,\n username: meta.username\n })\n }\n }\n }\n },\n 'gi.contracts/group/inviteRevoke': {\n validate: (data, { state, meta }) => {\n objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n })(data)\n\n if (!state.invites[data.inviteSecret]) {\n throw new TypeError(L('The link does not exist.'))\n }\n },\n process ({ data, meta }, { state }) {\n const invite = state.invites[data.inviteSecret]\n Vue.set(invite, 'status', INVITE_STATUS.REVOKED)\n }\n },\n 'gi.contracts/group/updateSettings': {\n // OPTIMIZE: Make this custom validation function\n // reusable accross other future validators\n validate: objectMaybeOf({\n groupName: x => typeof x === 'string',\n groupPicture: x => typeof x === 'string',\n sharedValues: x => typeof x === 'string',\n mincomeAmount: x => typeof x === 'number' && x > 0,\n mincomeCurrency: x => typeof x === 'string'\n }),\n process ({ meta, data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/group/groupProfileUpdate': {\n validate: objectMaybeOf({\n incomeDetailsType: x => ['incomeAmount', 'pledgeAmount'].includes(x),\n incomeAmount: x => typeof x === 'number' && x >= 0,\n pledgeAmount: x => typeof x === 'number' && x >= 0,\n nonMonetaryAdd: string,\n nonMonetaryEdit: objectOf({\n replace: string,\n with: string\n }),\n nonMonetaryRemove: string,\n paymentMethods: arrayOf(\n objectOf({\n name: string,\n value: string\n })\n )\n }),\n process ({ data, meta }, { state, getters }) {\n const groupProfile = state.profiles[meta.username]\n const nonMonetary = groupProfile.nonMonetaryContributions\n for (const key in data) {\n const value = data[key]\n switch (key) {\n case 'nonMonetaryAdd':\n nonMonetary.push(value)\n break\n case 'nonMonetaryRemove':\n nonMonetary.splice(nonMonetary.indexOf(value), 1)\n break\n case 'nonMonetaryEdit':\n nonMonetary.splice(nonMonetary.indexOf(value.replace), 1, value.with)\n break\n default:\n Vue.set(groupProfile, key, value)\n }\n }\n if (data.incomeDetailsType) {\n // someone updated their income details, create a snapshot of the haveNeeds\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n },\n 'gi.contracts/group/updateAllVotingRules': {\n validate: objectMaybeOf({\n ruleName: x => [RULE_PERCENTAGE, RULE_DISAGREEMENT].includes(x),\n ruleThreshold: number,\n expires_ms: number\n }),\n process ({ data, meta }, { state }) {\n // Update all types of proposal settings for simplicity\n if (data.ruleName && data.ruleThreshold) {\n for (const proposalSettings in state.settings.proposals) {\n Vue.set(state.settings.proposals[proposalSettings], 'rule', data.ruleName)\n Vue.set(state.settings.proposals[proposalSettings].ruleSettings[data.ruleName], 'threshold', data.ruleThreshold)\n }\n }\n\n // TODO later - support update expires_ms\n // if (data.ruleName && data.expires_ms) {\n // for (const proposalSetting in state.settings.proposals) {\n // Vue.set(state.settings.proposals[proposalSetting].ruleSettings[data.ruleName], 'expires_ms', data.expires_ms)\n // }\n // }\n }\n },\n 'gi.contracts/group/addChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n attributes: chatRoomAttributesType\n }),\n process ({ data, meta }, { state }) {\n const { name, type, privacyLevel } = data.attributes\n Vue.set(state.chatRooms, data.chatRoomID, {\n creator: meta.username,\n name,\n type,\n privacyLevel,\n deletedDate: null,\n users: []\n })\n if (!state.generalChatRoomId) {\n Vue.set(state, 'generalChatRoomId', data.chatRoomID)\n }\n }\n },\n 'gi.contracts/group/deleteChatRoom': {\n validate: (data, { getters, meta }) => {\n objectOf({ chatRoomID: string })(data)\n\n if (getters.getChatRooms[data.chatRoomID].creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state }) {\n Vue.delete(state.chatRooms, data.chatRoomID)\n }\n },\n 'gi.contracts/group/leaveChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n member: string,\n leavingGroup: boolean // if kicker is exists, it means group leaving\n }),\n process ({ data, meta }, { state }) {\n Vue.set(state.chatRooms[data.chatRoomID], 'users',\n state.chatRooms[data.chatRoomID].users.filter(u => u !== data.member))\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (meta.username === rootState.loggedIn.username && !sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n const sendingData = data.leavingGroup\n ? { member: data.member }\n : { member: data.member, username: meta.username }\n await sbp('gi.actions/chatroom/leave', { contractID: data.chatRoomID, data: sendingData })\n }\n }\n },\n 'gi.contracts/group/joinChatRoom': {\n validate: objectMaybeOf({\n username: string,\n chatRoomID: string\n }),\n process ({ data, meta }, { state }) {\n const username = data.username || meta.username\n state.chatRooms[data.chatRoomID].users.push(username)\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n const username = data.username || meta.username\n if (username === rootState.loggedIn.username) {\n if (!sbp('okTurtles.data/get', 'JOINING_GROUP') || sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n // while users are joining chatroom, they don't need to leave chatrooms\n // this is similar to setting 'JOINING_GROUP' before joining group\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', data.chatRoomID)\n await sbp('chelonia/contract/sync', data.chatRoomID)\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', undefined)\n sbp('okTurtles.data/set', 'READY_TO_JOIN_CHATROOM', false)\n }\n }\n }\n },\n 'gi.contracts/group/renameChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n name: string\n }),\n process ({ data, meta }, { state, getters }) {\n Vue.set(state.chatRooms, data.chatRoomID, {\n ...getters.getChatRooms[data.chatRoomID],\n name: data.name\n })\n }\n },\n ...((process.env.NODE_ENV === 'development' || process.env.CI) && {\n 'gi.contracts/group/forceDistributionDate': {\n validate: optional,\n process ({ meta }, { state, getters }) {\n getters.groupSettings.distributionDate = dateToPeriodStamp(meta.createdDate)\n }\n },\n 'gi.contracts/group/malformedMutation': {\n validate: objectOf({ errorType: string, sideEffect: optional(boolean) }),\n process ({ data }) {\n const ErrorType = Errors[data.errorType]\n if (data.sideEffect) return\n if (ErrorType) {\n throw new ErrorType('malformedMutation!')\n } else {\n throw new Error(`unknown error type: ${data.errorType}`)\n }\n },\n sideEffect (message, { state }) {\n if (!message.data.sideEffect) return\n sbp('gi.contracts/group/malformedMutation/process', {\n ...message,\n data: omit(message.data, ['sideEffect'])\n }, state)\n }\n }\n })\n // TODO: remove group profile when leave group is implemented\n },\n // methods are SBP selectors that are version-tracked for each contract.\n // in other words, you can use them to define SBP selectors that will\n // contain functions that you can modify across different contract versions,\n // and when the contract calls them, it will use that specific version of the\n // method.\n //\n // They are useful when used in conjunction with pushSideEffect from process\n // functions.\n //\n // IMPORTANT: they MUST begin with the name of the contract.\n methods: {\n 'gi.contracts/group/archiveProposal': async function (contractID, proposalHash, proposal) {\n const { username } = sbp('state/vuex/state').loggedIn\n const key = `proposals/${username}/${contractID}`\n const proposals = await sbp('gi.db/archive/load', key) || []\n // newest at the front of the array, oldest at the back\n proposals.unshift([proposalHash, proposal])\n while (proposals.length > MAX_ARCHIVED_PROPOSALS) {\n proposals.pop()\n }\n await sbp('gi.db/archive/save', key, proposals)\n sbp('okTurtles.events/emit', PROPOSAL_ARCHIVED, [proposalHash, proposal])\n }\n }\n})\n"], + "mappings": ";;;;;;;;AAKA,IAAM,YAAY,CAAC;AACnB,IAAM,UAAU,CAAC;AACjB,IAAM,gBAAgB,CAAC;AACvB,IAAM,gBAAgB,CAAC;AACvB,IAAM,kBAAkB,CAAC;AACzB,IAAM,kBAAkB,CAAC;AAEzB,IAAM,eAAe;AAErB,aAAc,aAAa,MAAM;AAC/B,QAAM,SAAS,mBAAmB,QAAQ;AAC1C,MAAI,CAAC,UAAU,WAAW;AACxB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AAGA,aAAW,WAAW,CAAC,gBAAgB,WAAW,cAAc,SAAS,aAAa,GAAG;AACvF,QAAI,SAAS;AACX,iBAAW,UAAU,SAAS;AAC5B,YAAI,OAAO,QAAQ,UAAU,IAAI,MAAM;AAAO;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACA,SAAO,UAAU,UAAU,KAAK,QAAQ,QAAQ,OAAO,GAAG,IAAI;AAChE;AAEO,4BAA6B,UAAU;AAC5C,QAAM,eAAe,aAAa,KAAK,QAAQ;AAC/C,MAAI,iBAAiB,MAAM;AACzB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AACA,SAAO,aAAa;AACtB;AAEA,IAAM,qBAAqB;AAAA,EACzB,0BAA0B,SAAU,MAAM;AACxC,UAAM,aAAa,CAAC;AACpB,eAAW,YAAY,MAAM;AAC3B,YAAM,SAAS,mBAAmB,QAAQ;AAC1C,UAAI,UAAU,WAAW;AACvB,QAAC,SAAQ,QAAQ,QAAQ,KAAK,6DAA6D,WAAW;AAAA,MACxG,WAAW,OAAO,KAAK,cAAc,YAAY;AAC/C,YAAI,gBAAgB,WAAW;AAE7B,UAAC,SAAQ,QAAQ,QAAQ,KAAK,6CAA6C,gDAAgD;AAAA,QAC7H;AACA,cAAM,KAAK,UAAU,YAAY,KAAK;AACtC,mBAAW,KAAK,QAAQ;AAExB,YAAI,CAAC,QAAQ,SAAS;AACpB,kBAAQ,UAAU,EAAE,OAAO,CAAC,EAAE;AAAA,QAChC;AAEA,YAAI,aAAa,GAAG,gBAAgB;AAClC,aAAG,KAAK,QAAQ,QAAQ,KAAK;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,4BAA4B,SAAU,MAAM;AAC1C,eAAW,YAAY,MAAM;AAC3B,UAAI,CAAC,gBAAgB,WAAW;AAC9B,cAAM,IAAI,MAAM,0CAA0C,UAAU;AAAA,MACtE;AACA,aAAO,UAAU;AAAA,IACnB;AAAA,EACF;AAAA,EACA,2BAA2B,SAAU,MAAM;AACzC,QAAI,4BAA4B,OAAO,KAAK,IAAI,CAAC;AACjD,WAAO,IAAI,0BAA0B,IAAI;AAAA,EAC3C;AAAA,EACA,wBAAwB,SAAU,MAAM;AACtC,eAAW,YAAY,MAAM;AAC3B,UAAI,UAAU,WAAW;AACvB,cAAM,IAAI,MAAM,mDAAmD;AAAA,MACrE;AACA,sBAAgB,YAAY;AAAA,IAC9B;AAAA,EACF;AAAA,EACA,sBAAsB,SAAU,MAAM;AACpC,eAAW,YAAY,MAAM;AAC3B,aAAO,gBAAgB;AAAA,IACzB;AAAA,EACF;AAAA,EACA,oBAAoB,SAAU,KAAK;AACjC,WAAO,UAAU;AAAA,EACnB;AAAA,EACA,0BAA0B,SAAU,QAAQ;AAC1C,kBAAc,KAAK,MAAM;AAAA,EAC3B;AAAA,EACA,0BAA0B,SAAU,QAAQ,QAAQ;AAClD,QAAI,CAAC,cAAc;AAAS,oBAAc,UAAU,CAAC;AACrD,kBAAc,QAAQ,KAAK,MAAM;AAAA,EACnC;AAAA,EACA,4BAA4B,SAAU,UAAU,QAAQ;AACtD,QAAI,CAAC,gBAAgB;AAAW,sBAAgB,YAAY,CAAC;AAC7D,oBAAgB,UAAU,KAAK,MAAM;AAAA,EACvC;AACF;AAEA,mBAAmB,0BAA0B,kBAAkB;AAE/D,IAAO,iBAAQ;;;ACpFf;;;ACNA;AACA;;;ACcO,cAAe,GAAG,OAAO;AAC9B,QAAM,IAAI,CAAC;AACX,aAAW,KAAK,GAAG;AACjB,QAAI,CAAC,MAAM,SAAS,CAAC,GAAG;AACtB,QAAE,KAAK,EAAE;AAAA,IACX;AAAA,EACF;AACA,SAAO;AACT;AAEO,mBAAoB,KAAK;AAC9B,SAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;AACvC;AAEA,2BAA4B,KAAK;AAC/B,QAAM,gBAAgB,OAAO,OAAO,QAAQ;AAE5C,SAAO,iBAAiB,OAAO,UAAU,SAAS,KAAK,GAAG,MAAM,qBAAqB,OAAO,UAAU,SAAS,KAAK,GAAG,MAAM;AAC/H;AAEO,eAAgB,KAAK,KAAK;AAC/B,aAAW,OAAO,KAAK;AACrB,UAAM,QAAQ,kBAAkB,IAAI,IAAI,IAAI,UAAU,IAAI,IAAI,IAAI;AAClE,QAAI,SAAS,kBAAkB,IAAI,IAAI,GAAG;AACxC,YAAM,IAAI,MAAM,KAAK;AACrB;AAAA,IACF;AACA,QAAI,OAAO,SAAS,IAAI;AAAA,EAC1B;AACA,SAAO;AACT;AAuEO,2BAA4B,GAAG,GAAG;AACvC,MAAI,MAAM;AAAG,WAAO;AACpB,MAAI,MAAM,QAAQ,MAAM,QAAQ,OAAQ,MAAO,OAAQ;AAAI,WAAO;AAClE,MAAI,OAAO,MAAM;AAAU,WAAO,MAAM;AACxC,MAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,QAAI,EAAE,WAAW,EAAE;AAAQ,aAAO;AAAA,EACpC,WAAW,EAAE,YAAY,SAAS,UAAU;AAC1C,UAAM,IAAI,MAAM,kBAAkB,GAAG;AAAA,EACvC;AACA,aAAW,OAAO,GAAG;AACnB,QAAI,CAAC,kBAAkB,EAAE,MAAM,EAAE,IAAI;AAAG,aAAO;AAAA,EACjD;AACA,SAAO;AACT;;;AD5HO,IAAM,gBAAgB;AAAA,EAC3B,cAAc,CAAC,OAAO;AAAA,EACtB,cAAc,CAAC,KAAK,MAAM,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EAEtF,qBAAqB;AACvB;AAEA,IAAM,YAAY,CAAC,IAAI,YAAY;AACjC,MAAI,QAAQ,aAAa,QAAQ,OAAO;AACtC,QAAI,SAAS;AACb,QAAI,QAAQ,QAAQ,KAAK;AACvB,eAAS,UAAU,MAAM;AACzB,aAAO,aAAa,KAAK,QAAQ,QAAQ;AACzC,aAAO,aAAa,KAAK,GAAG;AAAA,IAC9B;AACA,OAAG,cAAc;AACjB,OAAG,YAAY,UAAU,SAAS,QAAQ,OAAO,MAAM,CAAC;AAAA,EAC1D;AACF;AAWA,IAAI,UAAU,aAAa;AAAA,EACzB,MAAM;AAAA,EACN,QAAQ;AACV,CAAC;;;AElDD;AACA;;;ACNA,IAAM,QAAQ;AAEC,kBAAmB,YAAmB,MAAqB;AACxE,QAAM,WAAW,KAAK;AAGtB,QAAM,oBACH,OAAO,aAAa,YAAY,aAAa,OAC1C,WACA;AAGN,SAAO,QAAO,QAAQ,OAAO,oBAAqB,OAAO,SAAS,OAAO;AAEvE,QAAI,QAAO,QAAQ,OAAO,OAAO,QAAO,QAAQ,MAAM,YAAY,KAAK;AACrE,aAAO;AAAA,IACT;AAEA,UAAM,mBAGJ,OAAO,UAAU,eAAe,KAAK,mBAAmB,OAAO,IAC3D,kBAAkB,WAClB;AAGN,QAAI,qBAAqB,QAAQ,qBAAqB,QAAW;AAC/D,aAAO;AAAA,IACT;AACA,WAAO,OAAO,gBAAgB;AAAA,EAChC,CAAC;AACH;;;ADtBA,KAAI,UAAU,IAAI;AAClB,KAAI,UAAU,QAAQ;AAEtB,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,0BAAgD,CAAC;AAOvD,IAAM,kBAAkB;AAAA,EACtB,GAAG;AAAA,EACH,cAAc,CAAC,SAAS,QAAQ,OAAO,QAAQ;AAAA,EAC/C,cAAc,CAAC,KAAK,KAAK,MAAM,UAAU,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EACrG,qBAAqB;AACvB;AAEA,IAAI,kBAAkB;AACtB,IAAI,sBAAsB,gBAAgB,MAAM,GAAG,EAAE;AACrD,IAAI,0BAA0B;AAY9B,eAAI,0BAA0B;AAAA,EAC5B,qBAAqB,oBAAqB,UAAiC;AAEzE,UAAM,CAAC,gBAAgB,SAAS,YAAY,EAAE,MAAM,GAAG;AAGvD,QAAI,SAAS,YAAY,MAAM,gBAAgB,YAAY;AAAG;AAI9D,QAAI,iBAAiB;AAAqB;AAG1C,QAAI,iBAAiB,qBAAqB;AACxC,wBAAkB;AAClB,4BAAsB;AACtB,gCAA0B;AAC1B;AAAA,IACF;AACA,QAAI;AACF,gCAA2B,MAAM,eAAI,4BAA4B,QAAQ,KAAM;AAG/E,wBAAkB;AAClB,4BAAsB;AAAA,IACxB,SAAS,OAAP;AACA,cAAQ,MAAM,KAAK;AAAA,IACrB;AAAA,EACF;AACF,CAAC;AA0CM,kBAAmB,MAAiC;AACzD,QAAM,IAAI;AAAA,IACR,OAAO;AAAA,EACT;AACA,aAAW,OAAO,MAAM;AACtB,MAAE,GAAG,UAAU,IAAI;AACnB,MAAE,IAAI,SAAS,KAAK;AAAA,EACtB;AACA,SAAO;AACT;AAEe,WACb,KACA,MACQ;AACR,SAAO,SAAS,wBAAwB,QAAQ,KAAK,IAAI,EAEtD,QAAQ,iBAAiB,QAAQ;AACtC;AAgBA,kBAAmB,aAAa;AAC9B,SAAO,WAAU,SAAS,aAAa,eAAe;AACxD;AAEA,KAAI,UAAU,QAAQ;AAAA,EACpB,YAAY;AAAA,EACZ,OAAO;AAAA,IACL,MAAM,CAAC,QAAQ,KAAK;AAAA,IACpB,KAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA,SAAS;AAAA,EACX;AAAA,EACA,QAAQ,SAAU,GAAG,SAAS;AAC5B,UAAM,OAAO,QAAQ,SAAS,GAAG;AACjC,UAAM,cAAc,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,CAAC;AACpD,QAAI,CAAC,aAAa;AAChB,cAAQ,KAAK,yDAAyD,IAAI;AAC1E,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,IAAI;AAAA,IAChD;AAEA,QAAI,QAAQ,MAAM,QAAQ,OAAO,QAAQ,KAAK,MAAM,WAAW,UAAU;AACvE,cAAQ,KAAK,MAAM,MAAM;AAAA,IAC3B;AACA,QAAI,QAAQ,MAAM,SAAS;AACzB,YAAM,SAAS,KAAI,QAAQ,WAAW,SAAS,WAAW,IAAI,SAAS;AAEvE,aAAO,OAAO,OAAO,KAAK;AAAA,QACxB,IAAI,CAAC,QAAQ,SAAS;AACpB,cAAI,QAAQ,QAAQ;AAClB,mBAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,GAAG,IAAI;AAAA,UACnD,OAAO;AACL,mBAAO,EAAE,KAAK,GAAG,IAAI;AAAA,UACvB;AAAA,QACF;AAAA,QACA,IAAI,OAAK;AAAA,MACX,CAAC;AAAA,IACH,OAAO;AACL,UAAI,CAAC,QAAQ,KAAK;AAAU,gBAAQ,KAAK,WAAW,CAAC;AACrD,cAAQ,KAAK,SAAS,YAAY,SAAS,WAAW;AACtD,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,IAAI;AAAA,IAC1C;AAAA,EACF;AACF,CAAC;;;AE/LD;AAAA;AAAA;AAAA;AAAA;AAEO,IAAM,sBAAN,cAAkC,MAAM;AAAA,EAG7C,eAAgB,QAAe;AAC7B,UAAM,GAAG,MAAM;AACf,SAAK,OAAO;AACZ,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,KAAK,WAAW;AAAA,IAChD;AAAA,EACF;AACF;AAGO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC/C,eAAgB,QAAe;AAC7B,UAAM,GAAG,MAAM;AAEf,SAAK,OAAO;AACZ,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,KAAK,WAAW;AAAA,IAChD;AAAA,EACF;AACF;;;AC2BO,IAAM,cAAc,OAAO,SAAS;AACpC,IAAM,UAAU,OAAK,MAAM;AAC3B,IAAM,QAAQ,OAAK,MAAM;AACzB,IAAM,UAAU,OAAK,OAAO,MAAM;AAClC,IAAM,YAAY,OAAK,OAAO,MAAM;AACpC,IAAM,WAAW,OAAK,OAAO,MAAM;AACnC,IAAM,WAAW,OAAK,OAAO,MAAM;AACnC,IAAM,WAAW,OAAK,CAAC,MAAM,CAAC,KAAK,OAAO,MAAM;AAChD,IAAM,aAAa,OAAK,OAAO,MAAM;AAcrC,IAAM,UAAU,CAAC,QAAQ,aAAa;AAC3C,MAAI,WAAW,OAAO,IAAI;AAAG,WAAO,OAAO,KAAK,QAAQ;AACxD,SAAO,OAAO,QAAQ;AACxB;AAGO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YACE,SACA,cACA,WACA,OACA,WAAmB,IACnB,YAAqB,IACrB;AACA,UAAM,aAAa,WACjB,YAAY,0BAA0B,YAAY;AACpD,UAAM,UAAU;AAChB,SAAK,eAAe;AACpB,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,YAAY,aAAa;AAC9B,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,UAAU,GAAG;AAAA,EAAe,KAAK,aAAa;AACnD,SAAK,OAAO,KAAK,YAAY;AAC7B,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,kBAAkB;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,gBAAyB;AACvB,UAAM,YAAY,KAAK,MAAM,MAAM,gCAAgC,KAAK,CAAC;AACzE,WAAO,UAAU,KAAK,cAAY,SAAS,QAAQ,qBAAqB,MAAM,EAAE,KAAK;AAAA,EACvF;AAAA,EAEA,eAAwB;AACtB,WAAO;AAAA,eACI,KAAK;AAAA,eACL,KAAK;AAAA,eACL,KAAK,aAAa,QAAQ,OAAO,EAAE;AAAA,eACnC,KAAK;AAAA,eACL,KAAK;AAAA;AAAA,EAElB;AACF;AAKA,IAAM,iBAAoB,CACxB,QACA,OACA,OACA,SACA,cACA,cACuB;AACvB,SAAO,IAAI,mBACT,SACA,gBAAgB,QAAQ,MAAM,GAC9B,aAAa,OAAO,OACpB,KAAK,UAAU,KAAK,GACpB,OAAO,MACP,KACF;AACF;AAEO,IAAM,UACR,CAAC,QAA0B,SAAkB,YAAmC;AACjF,iBAAgB,OAAO;AACrB,QAAI,QAAQ,KAAK;AAAG,aAAO,CAAC,OAAO,KAAK,CAAC;AACzC,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,UAAI,QAAQ;AACZ,aAAO,MAAM,IAAI,OAAK,OAAO,GAAG,GAAG,UAAU,UAAU,CAAC;AAAA,IAC1D;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,SAAS,QAAQ,MAAM;AAC1C,SAAO;AACT;AAEK,IAAM,YACM,CAAC,cAAmC;AACnD,mBAAkB,OAAO,SAAS,IAAI;AACpC,QAAI,QAAQ,KAAK,KAAM,UAAU;AAAY,aAAO;AACpD,UAAM,eAAe,SAAS,OAAO,MAAM;AAAA,EAC7C;AACA,UAAQ,OAAO,MAAM;AACnB,QAAI,UAAU,SAAS;AAAG,aAAO,GAAG,YAAY,SAAS;AAAA;AACpD,aAAO,IAAI;AAAA,EAClB;AACA,SAAO;AACT;AAEK,IAAM,QAAc,CACzB,WACA,WAC8B;AAC9B,kBAAgB,OAAO;AACrB,QAAI,QAAQ,KAAK;AAAG,aAAO,CAAC;AAC5B,UAAM,IAAI,OAAO,KAAK;AACtB,UAAM,UAAU,CAAC,KAAK,QACpB,OAAO,OACL,KACA;AAAA,MAEE,CAAC,UAAU,KAAK,QAAQ,IAAI,OAAO,EAAE,MAAM,OAAO,KAAK;AAAA,IACzD,CACF;AACF,WAAO,OAAO,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC,CAAC;AAAA,EAC1C;AACA,SAAM,OAAO,MAAM,QAAQ,QAAQ,SAAS,OAAO,QAAQ,MAAM;AACjE,SAAO;AACT;AAoBO,IAAM,SACX,SAAU,OAAO;AACf,MAAI,QAAQ,KAAK;AAAG,WAAO,CAAC;AAC5B,MAAI,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,KAAK,GAAG;AAC5C,WAAO,OAAO,OAAO,CAAC,GAAG,KAAK;AAAA,EAChC;AACA,QAAM,eAAe,QAAQ,KAAK;AACpC;AAGK,IAAM,WACX,CAAC,SAAY,SAAkB,aAAoE;AACnG,mBAAkB,OAAO;AACvB,UAAM,IAAI,OAAO,KAAK;AACtB,UAAM,YAAY,OAAO,KAAK,OAAO;AACrC,UAAM,cAAc,OAAO,KAAK,CAAC,EAAE,KAAK,UAAQ,CAAC,UAAU,SAAS,IAAI,CAAC;AACzE,QAAI,aAAa;AACf,YAAM,eACJ,SACA,OACA,QACA,4BAA4B,mBAAmB,aACjD;AAAA,IACF;AACA,UAAM,YAAY,UAAU,KAAK,cAAY;AAC3C,YAAM,iBAAiB,QAAQ;AAC/B,aAAQ,eAAe,SAAS,WAAW,CAAC,EAAE,eAAe,QAAQ;AAAA,IACvE,CAAC;AACD,QAAI,WAAW;AACb,YAAM,eACJ,SACA,EAAE,YACF,GAAG,UAAU,aACb,0BAA0B,kBAAkB,eAC5C,iBAAiB,QAAQ,QAAQ,UAAU,EAAE,OAAO,CAAC,KACrD,GACF;AAAA,IACF;AAEA,UAAM,UAAU,QAAQ,KAAK,IACzB,CAAC,KAAK,QAAQ,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,CAAC,IAC/D,CAAC,KAAK,QAAQ;AACd,YAAM,SAAS,QAAQ;AACvB,UAAI,OAAO,SAAS,cAAc,CAAC,EAAE,eAAe,GAAG,GAAG;AACxD,eAAO,OAAO,OAAO,KAAK,CAAC,CAAC;AAAA,MAC9B,OAAO;AACL,eAAO,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,OAAO,EAAE,MAAM,GAAG,UAAU,KAAK,EAAE,CAAC;AAAA,MACzE;AAAA,IACF;AACF,WAAO,UAAU,OAAO,SAAS,CAAC,CAAC;AAAA,EACrC;AACA,UAAQ,OAAO,MAAM;AACnB,UAAM,QAAQ,OAAO,KAAK,OAAO,EAAE,IACjC,CAAC,QAAQ,QAAQ,KAAK,SAAS,aAC3B,GAAG,SAAS,QAAQ,QAAQ,MAAM,EAAE,QAAQ,KAAK,CAAC,MAClD,GAAG,QAAQ,QAAQ,QAAQ,IAAI,GACrC;AACA,WAAO;AAAA,GAAQ,MAAM,KAAK,OAAO;AAAA;AAAA,EACnC;AACA,SAAO;AACT;AAGO,uBAAwB,aAAqB,SAAkB,UAAkB;AACtF,SAAO,SAAU,MAAW;AAC1B,WAAO,IAAI;AACX,eAAW,OAAO,MAAM;AACtB,kBAAY,OAAO,KAAK,MAAM,GAAG,UAAU,KAAK;AAAA,IAClD;AACA,WAAO;AAAA,EACT;AACF;AAEO,IAAM,WACR,CAAC,WAAsD;AACxD,QAAM,UAAU,QAAQ,QAAQ,KAAK;AACrC,qBAAmB,GAAG;AACpB,WAAO,QAAQ,CAAC;AAAA,EAClB;AACA,YAAS,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,QAAQ,OAAO,IAAI,QAAQ,MAAM;AAC3E,SAAO;AACT;AAUK,eAAgB,OAAO,SAAS,IAAI;AACzC,MAAI,QAAQ,KAAK,KAAK,QAAQ,KAAK;AAAG,WAAO;AAC7C,QAAM,eAAe,OAAO,OAAO,MAAM;AAC3C;AACA,MAAM,OAAO,MAAM;AAGZ,IAAM,UACX,kBAAkB,OAAO,SAAS,IAAI;AACpC,MAAI,QAAQ,KAAK;AAAG,WAAO;AAC3B,MAAI,UAAU,KAAK;AAAG,WAAO;AAC7B,QAAM,eAAe,UAAS,OAAO,MAAM;AAC7C;AAIK,IAAM,SACX,iBAAiB,OAAO,SAAS,IAAI;AACnC,MAAI,QAAQ,KAAK;AAAG,WAAO;AAC3B,MAAI,SAAS,KAAK;AAAG,WAAO;AAC5B,QAAM,eAAe,SAAQ,OAAO,MAAM;AAC5C;AAIK,IAAM,SACX,iBAAiB,OAAO,SAAS,IAAI;AACnC,MAAI,QAAQ,KAAK;AAAG,WAAO;AAC3B,MAAI,SAAS,KAAK;AAAG,WAAO;AAC5B,QAAM,eAAe,SAAQ,OAAO,MAAM;AAC5C;AAiBF,qBAAsB,WAAW;AAC/B,iBAAgB,OAAc,SAAS,IAAI;AACzC,UAAM,cAAc,UAAU;AAC9B,QAAI,QAAQ,KAAK;AAAG,aAAO,UAAU,IAAI,QAAM,GAAG,KAAK,CAAC;AACxD,QAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,aAAa;AACxD,YAAM,aAAa,CAAC;AACpB,eAAS,IAAI,GAAG,IAAI,aAAa,KAAK,GAAG;AACvC,mBAAW,KAAK,UAAU,GAAG,MAAM,IAAI,MAAM,CAAC;AAAA,MAChD;AACA,aAAO;AAAA,IACT;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,IAAI,UAAU,IAAI,QAAM,QAAQ,EAAE,CAAC,EAAE,KAAK,IAAI;AACjE,SAAO;AACT;AAIO,IAAM,UAAU;AAcvB,qBAAsB,WAAW;AAC/B,iBAAgB,OAAc,SAAS,IAAI;AACzC,eAAW,UAAU,WAAW;AAC9B,UAAI;AACF,eAAO,OAAO,OAAO,MAAM;AAAA,MAC7B,SAAS,GAAP;AAAA,MAAW;AAAA,IACf;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,IAAI,UAAU,IAAI,QAAM,QAAQ,EAAE,CAAC,EAAE,KAAK,KAAK;AAClE,SAAO;AACT;AAGO,IAAM,UAAU;;;ACpYhB,IAAM,yBAAyB;AAC/B,IAAM,gBAAgB;AAAA,EAC3B,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AACR;AACO,IAAM,iBAAiB;AAAA,EAC5B,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AACX;AAEO,IAAM,kBAAkB;AACxB,IAAM,yBAAyB;AAC/B,IAAM,yBAAyB;AAC/B,IAAM,gCAAgC;AACtC,IAAM,mCAAmC;AACzC,IAAM,mBAAmB;AAEzB,IAAM,oBAAoB;AAC1B,IAAM,yBAAyB;AAE/B,IAAM,cAAc;AACpB,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AAEtB,IAAM,mBAAmB;AAgBzB,IAAM,iBAAiB;AAAA,EAC5B,YAAY;AAAA,EACZ,OAAO;AACT;AAEO,IAAM,yBAAyB;AAAA,EACpC,OAAO;AAAA,EACP,SAAS;AAAA,EACT,QAAQ;AACV;AAEO,IAAM,gBAAgB;AAAA,EAC3B,MAAM;AAAA,EACN,MAAM;AAAA,EACN,aAAa;AAAA,EACb,cAAc;AAChB;AAEO,IAAM,yBAAyB;AAAA,EACpC,aAAa;AAAA,EACb,UAAU;AACZ;AAEO,IAAM,wBAAwB;AAAA,EACnC,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,MAAM;AACR;AAWO,IAAM,oBAAoB;AAC1B,IAAM,uBAAuB;;;ACvF7B,IAAM,eAAe;AACrB,IAAM,mBAAmB;AACzB,IAAM,iBAAiB;AACvB,IAAM,WAAW;AAEjB,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAC1B,IAAM,oBAAoB;AAejC,IAAM,gBAAgB,CAAC,UAAU,OAAO,KAAK,MAAM,QAAQ,EAAE,OAAO,OAAK,MAAM,SAAS,GAAG,WAAW,eAAe,MAAM,EAAE;AAE7H,IAAM,QAAgB;AAAA,EACpB,CAAC,kBAAkB,SAAU,OAAO,eAAc,OAAO;AACvD,YAAQ,OAAO,OAAO,KAAK;AAC3B,QAAI,aAAa,cAAc,KAAK;AACpC,QAAI,kBAAiB;AAAwB,oBAAc;AAC3D,UAAM,mBAAmB,MAAM,SAAS,UAAU,eAAc,aAAa,iBAAiB;AAC9F,UAAM,YAAY,qBAAqB,iBAAiB,kBAAkB,UAAU;AACpF,UAAM,mBAAmB,MAAM,OAAO,OAAK,MAAM,gBAAgB,EAAE;AACnE,UAAM,WAAW,MAAM,OAAO,OAAK,MAAM,QAAQ,EAAE;AACnD,UAAM,eAAe,MAAM,OAAO,OAAK,MAAM,YAAY,EAAE;AAC3D,UAAM,oBAAoB,WAAW;AACrC,UAAM,UAAU,oBAAoB;AACpC,UAAM,SAAS,aAAa;AAK5B,UAAM,eAAe,KAAK,KAAK,YAAa,cAAa,iBAAiB;AAC1E,YAAQ,MAAM,cAAc,uBAAuB,kBAAiB,EAAE,cAAc,UAAU,cAAc,kBAAkB,WAAW,QAAQ,SAAS,WAAW,CAAC;AACtK,QAAI,YAAY,cAAc;AAC5B,aAAO;AAAA,IACT;AACA,WAAO,WAAW,SAAS,eAAe,eAAe;AAAA,EAC3D;AAAA,EACA,CAAC,oBAAoB,SAAU,OAAO,eAAc,OAAO;AACzD,YAAQ,OAAO,OAAO,KAAK;AAC3B,UAAM,aAAa,cAAc,KAAK;AACtC,UAAM,aAAa,kBAAiB,yBAAyB,IAAI;AACjE,UAAM,oBAAoB,KAAK,IAAI,MAAM,SAAS,UAAU,eAAc,aAAa,mBAAmB,WAAW,UAAU;AAC/H,UAAM,YAAY,qBAAqB,mBAAmB,mBAAmB,UAAU;AACvF,UAAM,WAAW,MAAM,OAAO,OAAK,MAAM,QAAQ,EAAE;AACnD,UAAM,eAAe,MAAM,OAAO,OAAK,MAAM,YAAY,EAAE;AAC3D,UAAM,UAAU,MAAM;AACtB,UAAM,SAAS,aAAa;AAE5B,YAAQ,MAAM,cAAc,yBAAyB,kBAAiB,EAAE,UAAU,cAAc,WAAW,SAAS,YAAY,OAAO,CAAC;AACxI,QAAI,gBAAgB,WAAW;AAC7B,aAAO;AAAA,IACT;AAGA,WAAO,eAAe,SAAS,YAAY,WAAW;AAAA,EACxD;AAAA,EACA,CAAC,oBAAoB,SAAU,OAAO,eAAc,OAAO;AACzD,UAAM,IAAI,MAAM,gBAAgB;AAAA,EAMlC;AACF;AAEA,IAAO,gBAAQ;AAER,IAAM,WAAgB,QAAQ,GAAG,OAAO,KAAK,KAAK,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAc1E,IAAM,uBAAuB,CAAC,MAAc,WAAmB,cAA8B;AAClG,QAAM,kBAAkB,KAAK,IAAI,GAAG,SAAS;AAE7C,SAAO;AAAA,IACL,CAAC,oBAAoB,MAAM;AAEzB,aAAO,KAAK,IAAI,kBAAkB,GAAG,SAAS;AAAA,IAChD;AAAA,IACA,CAAC,kBAAkB,MAAM;AAEvB,YAAM,eAAe,IAAI;AACzB,aAAO,KAAK,IAAI,cAAc,SAAS;AAAA,IACzC;AAAA,EACF,EAAE,MAAM;AACV;;;AC/GO,IAAM,cAAc;AACpB,IAAM,eAAe,KAAK;AAC1B,IAAM,cAAc,KAAK;AACzB,IAAM,gBAAgB,KAAK;AAa3B,2BAA4B,MAA6B;AAC9D,SAAO,IAAI,KAAK,IAAI,EAAE,YAAY;AACpC;AAEO,6BAA8B,UAAwB;AAC3D,SAAO,IAAI,KAAK,QAAQ;AAC1B;AAEO,8BAA+B,EAAE,YAAY,aAAa,gBAEtD;AACT,QAAM,kBAAkB,oBAAoB,WAAW;AACvD,MAAI,aAAa,cAAc,iBAAiB,YAAY;AAC5D,QAAM,UAAU,IAAI,KAAK,UAAU;AACnC,MAAI;AACJ,MAAI,UAAU,YAAY;AACxB,QAAI,WAAW,iBAAiB;AAC9B,aAAO;AAAA,IACT,OAAO;AAEL,kBAAY;AACZ,SAAG;AACD,oBAAY,cAAc,WAAW,CAAC,YAAY;AAAA,MACpD,SAAS,UAAU;AAAA,IACrB;AAAA,EACF,OAAO;AAEL,OAAG;AACD,kBAAY;AACZ,mBAAa,cAAc,YAAY,YAAY;AAAA,IACrD,SAAS,WAAW;AAAA,EACtB;AACA,SAAO,kBAAkB,SAAS;AACpC;AAEO,4BAA6B,EAAE,MAAM,aAAa,gBAE7C;AACV,QAAM,UAAU,IAAI,KAAK,IAAI;AAC7B,QAAM,QAAQ,oBAAoB,WAAW;AAC7C,SAAO,UAAU,SAAS,UAAU,cAAc,OAAO,YAAY;AACvE;AAEO,uBAAwB,MAAqB,YAA0B;AAC5E,QAAM,IAAI,IAAI,KAAK,IAAI;AACvB,IAAE,QAAQ,EAAE,QAAQ,IAAI,UAAU;AAClC,SAAO;AACT;AA0BO,6BAA8B,SAAiB,SAAyB;AAC7E,SAAO,oBAAoB,OAAO,EAAE,QAAQ,IAAI,oBAAoB,OAAO,EAAE,QAAQ;AACvF;AASO,8BAA+B,GAAW,GAAmB;AAClE,SAAO,IAAI,KAAK,CAAC,EAAE,QAAQ,IAAI,IAAI,KAAK,CAAC,EAAE,QAAQ;AACrD;AA0BO,uBAAwB,KAAsB;AACnD,SAAO,6CAA6C,KAAK,GAAG;AAC9D;;;ACjHO,yBAA0B,EAAE,OAAO,cAAc,UAAU,cAAc;AAC9E,WAAI,OAAO,MAAM,WAAW,YAAY;AACxC,iBAAI,qCAAqC,YACvC,CAAC,sCAAsC,YAAY,cAAc,QAAQ,CAC3E;AACF;AAMO,IAAM,uBAA4B,SAAS;AAAA,EAChD,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,cAAc,SAAS;AAAA,IACrB,CAAC,kBAAkB,SAAS,EAAE,WAAW,OAAO,CAAC;AAAA,IACjD,CAAC,oBAAoB,SAAS,EAAE,WAAW,OAAO,CAAC;AAAA,EACrD,CAAC;AACH,CAAC;AAgBD,qBAAsB,OAAY,EAAE,MAAM,MAAM,cAAmB;AACjE,QAAM,EAAE,iBAAiB;AACzB,QAAM,WAAW,MAAM,UAAU;AACjC,WAAS,SAAS;AAClB,iBAAI,yBAAyB,iBAAiB,OAAO,cAAc,IAAI;AACvE,kBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAC/D;AAIO,IAAM,mBAAmB;AAAA,EAC9B,MAAM;AAAA,EACN,YAAY,KAAK;AAAA,EACjB,cAAe;AAAA,IACb,CAAC,kBAAkB,EAAE,WAAW,KAAK;AAAA,IACrC,CAAC,oBAAoB,EAAE,WAAW,EAAE;AAAA,EACtC;AACF;AAEA,IAAM,YAAoB;AAAA,EACxB,CAAC,yBAAyB;AAAA,IACxB,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,UAAU,KAAK;AACxB,eAAS,SAAS;AAGlB,YAAM,UAAU,EAAE,MAAM,MAAM,KAAK,aAAa,WAAW;AAC3D,qBAAI,qCAAqC,SAAS,KAAK;AACvD,qBAAI,yBAAyB,iBAAiB,OAAO,UAAU,IAAI;AACnE,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IA+B/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,yBAAyB;AAAA,IACxB,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,cAAc,gBAAgB;AACtC,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,eAAS,UAAU;AACnB,YAAM,cAAc;AAAA,QAClB,GAAG,SAAS,KAAK;AAAA,QACjB;AAAA,QACA,iBAAiB;AAAA,MACnB;AACA,YAAM,UAAU,EAAE,MAAM,aAAa,MAAM,WAAW;AACtD,qBAAI,2CAA2C,SAAS,KAAK;AAC7D,qBAAI,qCAAqC,YACvC,CAAC,8CAA8C,OAAO,CACxD;AACA,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,gCAAgC;AAAA,IAC/B,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,YAAM,EAAE,SAAS,kBAAkB,SAAS,KAAK;AAGjD,YAAM,UAAU;AAAA,QACd;AAAA,QACA,MAAM,EAAE,CAAC,UAAU,cAAc;AAAA,QACjC;AAAA,MACF;AACA,qBAAI,6CAA6C,SAAS,KAAK;AAC/D,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,mCAAmC;AAAA,IAClC,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,YAAM,UAAU;AAAA,QACd;AAAA,QACA,MAAM,SAAS,KAAK;AAAA,QACpB;AAAA,MACF;AACA,qBAAI,mDAAmD,SAAS,KAAK;AACrE,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,mBAAmB;AAAA,IAClB,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,qBAAI,yBAAyB,iBAAiB,OAAO,UAAU,IAAI;AACnE,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AACF;AAEA,IAAO,oBAAQ;AAER,IAAM,eAAoB,QAAQ,GAAG,OAAO,KAAK,SAAS,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;;;AC5LlF,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAC1B,IAAM,gBAAgB;AACtB,IAAM,uBAAuB;AAC7B,IAAM,oBAAoB;AAC1B,IAAM,oBAA4B,QAAQ,GAAG,CAAC,iBAAiB,mBAAmB,eAAe,sBAAsB,iBAAiB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAChK,IAAM,sBAAsB;AAC5B,IAAM,uBAAuB;AAC7B,IAAM,sBAAsB;AAC5B,IAAM,cAAsB,QAAQ,GAAG,CAAC,qBAAqB,sBAAsB,mBAAmB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;;;ACNtH,6BAA8B,WAAiD;AAC5F,MAAI,YAAY;AAChB,MAAI,YAAY;AAChB,QAAM,SAAS,CAAC;AAChB,QAAM,UAAU,CAAC;AACjB,aAAW,YAAY,WAAW;AAChC,QAAI,SAAS,WAAW,GAAG;AACzB,aAAO,KAAK,QAAQ;AACpB,mBAAa,SAAS;AAAA,IACxB,WAAW,SAAS,WAAW,GAAG;AAChC,cAAQ,KAAK,QAAQ;AACrB,mBAAa,KAAK,IAAI,SAAS,QAAQ;AAAA,IACzC;AAAA,EACF;AACA,QAAM,eAAe,KAAK,IAAI,GAAG,YAAY,SAAS;AACtD,QAAM,WAAW,CAAC;AAClB,aAAW,SAAS,QAAQ;AAC1B,UAAM,qBAAqB,eAAe,MAAM;AAChD,eAAW,UAAU,SAAS;AAC5B,YAAM,kBAAkB,KAAK,IAAI,OAAO,QAAQ,IAAI;AACpD,eAAS,KAAK;AAAA,QACZ,QAAQ,qBAAqB;AAAA,QAC7B,MAAM,MAAM;AAAA,QACZ,IAAI,OAAO;AAAA,MACb,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;;;AC/Be,oCACb,cAC2D;AAC3D,QAAM,sBAAsB,CAAC;AAC7B,QAAM,iBAAiB,CAAC;AACxB,QAAM,eAAe,CAAC;AACtB,QAAM,gBAAgB,CAAC;AACvB,QAAM,wBAAwB,CAAC;AAC/B,aAAW,QAAQ,cAAc;AAC/B,wBAAoB,KAAK,MAAO,qBAAoB,KAAK,OAAO,KAAK,KAAK;AAC1E,mBAAe,KAAK,QAAS,gBAAe,KAAK,SAAS,KAAK,KAAK;AAAA,EACtE;AACA,aAAW,QAAQ,gBAAgB;AACjC,iBAAa,KAAK,EAAE,MAAM,QAAQ,eAAe,MAAM,CAAC;AAAA,EAC1D;AACA,aAAW,QAAQ,qBAAqB;AACtC,kBAAc,KAAK,EAAE,MAAM,QAAQ,oBAAoB,MAAM,CAAC;AAAA,EAChE;AAEA,eAAa,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AAC/C,gBAAc,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AAChD,SAAO,aAAa,SAAS,KAAK,cAAc,SAAS,GAAG;AAC1D,UAAM,YAAY,aAAa,IAAI;AACnC,UAAM,aAAa,cAAc,IAAI;AACrC,UAAM,OAAO,UAAU,SAAS,WAAW;AAC3C,QAAI,OAAO,GAAG;AAEZ,4BAAsB,KAAK,EAAE,QAAQ,UAAU,QAAQ,MAAM,UAAU,MAAM,IAAI,WAAW,KAAK,CAAC;AAClG,iBAAW,UAAU,UAAU;AAC/B,oBAAc,KAAK,UAAU;AAAA,IAC/B,WAAW,OAAO,GAAG;AAEnB,4BAAsB,KAAK,EAAE,QAAQ,WAAW,QAAQ,MAAM,UAAU,MAAM,IAAI,WAAW,KAAK,CAAC;AACnG,gBAAU,UAAU,WAAW;AAC/B,mBAAa,KAAK,SAAS;AAAA,IAC7B,OAAO;AAEL,4BAAsB,KAAK,EAAE,QAAQ,WAAW,QAAQ,MAAM,UAAU,MAAM,IAAI,WAAW,KAAK,CAAC;AAAA,IACrG;AAAA,EACF;AACA,SAAO;AACT;;;AC7BO,IAAM,eAAe;AAE5B,qBAAsB,OAAgC;AAEpD,SAAO,OAAO,UAAU,WAAW,MAAM,QAAQ,KAAK,GAAG,IAAI,MAAM,SAAS;AAC9E;AAEA,mBAAoB,IAAqB;AACvC,SAAO,CAAC,MAAO,KAAW,WAAW,EAAE,CAAC;AAC1C;AAEA,2BAA4B,IAAY,aAAqB;AAC3D,QAAM,WAAW,GAAG,MAAM,GAAG,EAAE;AAC/B,SAAO,CAAC,YAAY,SAAS,UAAU;AACzC;AAGA,yBAA0B,OAAe,aAAqB;AAC5D,QAAM,KAAK,YAAY,KAAK;AAC5B,SAAO,UAAU,EAAE,KAAK,kBAAkB,IAAI,WAAW;AAC3D;AAEA,uBAAwB,KAAa,aAA6B;AAEhE,SAAO,IAAI,QAAQ,WAAW,EAAE,QAAQ,SAAS,EAAE;AACrD;AAEO,oBAAqB,OAAuB;AAEjD,SAAO,WAAW,MAAM,QAAQ,YAAY,CAAC;AAC/C;AAYA,sBAAuB,SAAmB;AACxC,QAAM,EAAE,QAAQ,gBAAgB,aAAa,mBAAmB;AAChE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,qBAAqB,CAAC,MAAc,eAAe,cAAc,GAAG,WAAW,CAAC;AAAA,IAChF,wBAAwB,CAAC,MAAc,cAAc,GAAG,WAAW;AAAA,IACnE,UAAU,CAAC,MAAc,gBAAgB,GAAG,WAAW;AAAA,EACzD;AACF;AAMA,IAAM,aAAqC;AAAA,EACzC,KAAK,aAAa;AAAA,IAChB,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,gBAAgB,YAAU,MAAM;AAAA,EAClC,CAAC;AAAA,EACD,KAAK,aAAa;AAAA,IAChB,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,gBAAgB,YAAU,WAAM;AAAA,EAClC,CAAC;AAAA,EACD,KAAK,aAAa;AAAA,IAChB,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,gBAAgB,YAAU,SAAS;AAAA,EACrC,CAAC;AACH;AAEA,IAAO,qBAAQ;;;ACtFf,IAAM,UAAU,IAAI,KAAK,IAAI,IAAI,YAAY;AAEtC,gCAAiC,EAAE,YAAY,CAAC,GAAG,WAAW,QAEpD;AACf,QAAM,eAAe,oBAAoB,SAAS;AAClD,SAAO,WAAW,2BAA2B,YAAY,IAAI;AAC/D;AAEO,8BACL,EAAE,cAAc,UAAU,SACZ;AACd,iBAAe,UAAU,YAAY;AAErC,aAAW,QAAQ,cAAc;AAC/B,SAAK,QAAQ,KAAK;AAAA,EACpB;AACA,iBAAe,sBAAsB,cAAc,QAAQ,EAGxD,OAAO,UAAQ,KAAK,UAAU,OAAO;AACxC,aAAW,QAAQ,cAAc;AAC/B,SAAK,SAAS,WAAW,KAAK,MAAM;AACpC,SAAK,QAAQ,WAAW,KAAK,KAAK;AAClC,SAAK,UAAU,KAAK,UAAU,KAAK;AACnC,SAAK,SAAS;AACd,SAAK,QAAQ;AAAA,EACf;AAGA,SAAO;AACT;AAGA,4BAA6B,UAAsC;AAEjE,aAAW,UAAU,QAAQ;AAC7B,WAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,UAAM,WAAW,SAAS;AAC1B,aAAS,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AAC5C,YAAM,WAAW,SAAS;AAG1B,UAAK,SAAS,SAAS,SAAS,QAAQ,SAAS,OAAO,SAAS,MAC9D,SAAS,OAAO,SAAS,QAAQ,SAAS,SAAS,SAAS,IAAK;AAGlE,iBAAS,UAAW,UAAS,SAAS,SAAS,OAAO,IAAI,MAAM,SAAS;AACzE,iBAAS,SAAU,UAAS,SAAS,SAAS,OAAO,IAAI,MAAM,SAAS;AAExE,iBAAS,OAAO,GAAG,CAAC;AACpB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,0BAA2B,WAAyB,WAAuC;AACzF,SAAO,mBAAmB,CAAC,GAAG,WAAW,GAAG,SAAS,CAAC;AACxD;AAEA,+BAAgC,WAAyB,WAAuC;AAE9F,cAAY,UAAU,SAAS;AAE/B,aAAW,KAAK,WAAW;AACzB,MAAE,UAAU;AACZ,MAAE,SAAS;AAAA,EACb;AACA,SAAO,iBAAiB,WAAW,SAAS;AAC9C;;;AClEO,IAAM,aAAkB,SAAS;AAAA,EACtC,cAAc;AAAA,EACd,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS,SAAS,MAAM;AAAA,EACxB,QAAQ;AAAA,EACR,WAAW,MAAM,QAAQ,MAAM;AAAA,EAC/B,SAAS;AACX,CAAC;AAIM,IAAM,yBAA8B,SAAS;AAAA,EAClD,MAAM;AAAA,EACN,aAAa;AAAA,EACb,MAAM,QAAQ,GAAG,OAAO,OAAO,cAAc,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAAA,EACrE,cAAc,QAAQ,GAAG,OAAO,OAAO,sBAAsB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AACvF,CAAC;AAEM,IAAM,cAAmB,cAAc;AAAA,EAC5C,MAAM,QAAQ,GAAG,OAAO,OAAO,aAAa,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAAA,EACpE,MAAM;AAAA,EACN,cAAc,cAAc;AAAA,IAC1B,MAAM,QAAQ,GAAG,OAAO,OAAO,qBAAqB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAAA,IAC5E,QAAQ,MAAM,QAAQ,MAAM;AAAA,EAC9B,CAAC;AAAA,EACD,iBAAiB,SAAS;AAAA,IACxB,IAAI;AAAA,IACJ,MAAM;AAAA,EACR,CAAC;AAAA,EACD,WAAW,MAAM,QAAQ,QAAQ,MAAM,CAAC;AAAA,EACxC,eAAe,QAAQ,MAAM;AAE/B,CAAC;AAIM,IAAM,WAAgB,QAAQ,GAAG,CAAC,mBAAmB,oBAAoB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;;;ACjCxG,wBAAyB,KAAa,KAAa,cAAwB;AACzE,MAAI,QAAQ,IAAI;AAChB,MAAI,CAAC,OAAO;AACV,aAAI,IAAI,KAAK,KAAK,YAAY;AAC9B,YAAQ,IAAI;AAAA,EACd;AACA,SAAO;AACT;AAEA,0BAA2B,YAAoB,YAAoB;AACjE,SAAO;AAAA,IACL,gBAAgB;AAAA,IAChB;AAAA,IACA;AAAA,IACA,0BAA0B,CAAC;AAAA,IAC3B,QAAQ,eAAe;AAAA,IACvB,cAAc;AAAA,EAChB;AACF;AAEA,2BAA4B,EAAE,WAAW;AACvC,SAAO;AAAA,IAEL,iBAAiB,QAAQ;AAAA,IAMzB,qBAAqB;AAAA,IACrB,cAAc,CAAC;AAAA,IAIf,0BAA0B;AAAA,IAE1B,mBAAmB;AAAA,EACrB;AACF;AAIA,0BAA2B,EAAE,OAAO,WAAW;AAC7C,QAAM,mBAAmB,OAAO,KAAK,MAAM,gBAAgB,EAAE,KAAK;AAElE,SAAO,iBAAiB,SAAS,GAAG;AAClC,UAAM,SAAS,iBAAiB,MAAM;AACtC,eAAW,eAAe,QAAQ,uBAAuB,MAAM,GAAG;AAChE,eAAI,OAAO,MAAM,UAAU,WAAW;AAAA,IAExC;AACA,aAAI,OAAO,MAAM,kBAAkB,MAAM;AAAA,EAC3C;AACF;AAEA,iCAAkC,EAAE,MAAM,OAAO,WAAW;AAC1D,QAAM,SAAS,QAAQ,qBAAqB,KAAK,WAAW;AAC5D,QAAM,iBAAiB,eAAe,MAAM,kBAAkB,QAAQ,kBAAkB,EAAE,QAAQ,CAAC,CAAC;AACpG,mBAAiB,EAAE,OAAO,QAAQ,CAAC;AACnC,SAAO;AACT;AAIA,mCAAoC,EAAE,MAAM,OAAO,WAAW;AAC5D,QAAM,oBAAoB,wBAAwB,EAAE,MAAM,OAAO,QAAQ,CAAC;AAC1E,QAAM,SAAS,QAAQ,qBAAqB,KAAK,WAAW;AAC5D,QAAM,aAAa,OAAO,KAAK,kBAAkB,YAAY,EAAE,WAAW;AAE1E,MAAI,oBAAoB,QAAQ,QAAQ,cAAc,gBAAgB,IAAI,GAAG;AAC3E,YAAQ,cAAc,mBAAmB;AAAA,EAC3C;AAEA,MAAI,cAAc,CAAC,kBAAkB,mBAAmB;AACtD,sBAAkB,oBAAoB,QAAQ,uBAAuB,MAAM;AAAA,EAC7E;AAEA,MAAI,CAAC,YAAY;AACf,+BAA2B,EAAE,QAAQ,QAAQ,CAAC;AAAA,EAChD;AACF;AAEA,oCAAqC,EAAE,QAAQ,WAAW;AACxD,QAAM,WAAW,QAAQ,oBAAoB;AAC7C,MAAI,YAAY,SAAS,mBAAmB;AAC1C,UAAM,WAAW,QAAQ,cAAc;AACvC,aAAS,2BAA2B,qBAAqB;AAAA,MACvD,cAAc,uBAAuB,EAAE,WAAW,SAAS,mBAAmB,SAAS,CAAC;AAAA,MACxF,UAAU,QAAQ,kBAAkB,MAAM;AAAA,MAC1C,OAAO,QAAQ,iBAAiB,MAAM;AAAA,IACxC,CAAC,EAAE,OAAO,UAAQ;AAEhB,aAAO,QAAQ,aAAa,KAAK,EAAE,EAAE,WAAW,eAAe;AAAA,IACjE,CAAC;AAAA,EACH;AACF;AAEA,sBAAuB,EAAE,UAAU,YAAY,EAAE,MAAM,OAAO,WAAW;AACvE,QAAM,SAAS,UAAU,SAAS,eAAe;AACjD,QAAM,SAAS,UAAU,eAAe;AAExC,4BAA0B,EAAE,MAAM,OAAO,QAAQ,CAAC;AACpD;AAEA,iCAAkC,YAAoB,aAA+B;AAOnF,SAAO,QAAQ,WAAW,KAAK,qBAAqB,WAAW,aAAa,YAAY,UAAU,IAAI;AACxG;AAEA,eAAI,2BAA2B;AAAA,EAC7B,MAAM;AAAA,EACN,UAAU;AAAA,IACR,UAAU,SAAS;AAAA,MACjB,aAAa;AAAA,MACb,UAAU;AAAA,MACV,oBAAoB;AAAA,IACtB,CAAC;AAAA,IACD,SAAU;AACR,YAAM,EAAE,UAAU,uBAAuB,eAAI,kBAAkB,EAAE;AACjE,aAAO;AAAA,QAKL,aAAa,IAAI,KAAK,EAAE,YAAY;AAAA,QACpC;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAOA,SAAS;AAAA,IAMP,kBAAmB,OAAO;AACxB,aAAO;AAAA,IACT;AAAA,IACA,cAAe,OAAO,SAAS;AAC7B,aAAO,QAAQ,kBAAkB,YAAY,CAAC;AAAA,IAChD;AAAA,IACA,aAAc,OAAO,SAAS;AAC5B,aAAO,cAAY;AACjB,cAAM,WAAW,QAAQ,kBAAkB;AAC3C,eAAO,YAAY,SAAS;AAAA,MAC9B;AAAA,IACF;AAAA,IACA,cAAe,OAAO,SAAS;AAC7B,YAAM,WAAW,CAAC;AAClB,iBAAW,YAAa,QAAQ,kBAAkB,YAAY,CAAC,GAAI;AACjE,cAAM,UAAU,QAAQ,aAAa,QAAQ;AAC7C,YAAI,QAAQ,WAAW,eAAe,QAAQ;AAC5C,mBAAS,YAAY;AAAA,QACvB;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IACA,mBAAoB,OAAO,SAAS;AAClC,aAAO,QAAQ,cAAc;AAAA,IAC/B;AAAA,IACA,qBAAsB,OAAO,SAAS;AACpC,aAAO,QAAQ,cAAc;AAAA,IAC/B;AAAA,IACA,qBAAsB,OAAO,SAAS;AACpC,aAAO,CAAC,eAA8B;AACpC,YAAI,OAAO,eAAe,UAAU;AAClC,uBAAa,WAAW,YAAY;AAAA,QACtC;AACA,cAAM,EAAE,kBAAkB,6BAA6B,QAAQ;AAC/D,eAAO,qBAAqB;AAAA,UAC1B;AAAA,UACA,aAAa;AAAA,UACb,cAAc;AAAA,QAChB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,IACA,mBAAoB,OAAO,SAAS;AAClC,aAAO,CAAC,gBAAwB;AAC9B,cAAM,MAAM,QAAQ,cAAc;AAClC,eAAO,kBAAkB,cAAc,oBAAoB,WAAW,GAAG,CAAC,GAAG,CAAC;AAAA,MAChF;AAAA,IACF;AAAA,IACA,kBAAmB,OAAO,SAAS;AACjC,aAAO,CAAC,gBAAwB;AAC9B,cAAM,MAAM,QAAQ,cAAc;AAClC,eAAO,kBAAkB,cAAc,oBAAoB,WAAW,GAAG,GAAG,CAAC;AAAA,MAC/E;AAAA,IACF;AAAA,IACA,iBAAkB,OAAO,SAAS;AAChC,aAAO,CAAC,gBAAwB;AAC9B,eAAO,kBACL,cACE,oBAAoB,QAAQ,kBAAkB,WAAW,CAAC,GAC1D,CAAC,WACH,CACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,2BAA4B,OAAO,SAAS;AAC1C,aAAO,CAAC,UAAU,QAAQ,gBAAgB;AACxC,cAAM,WAAW,QAAQ,kBAAkB;AAC3C,cAAM,iBAAiB,QAAQ;AAC/B,cAAM,EAAE,cAAc,wBAAwB,eAAe,gBAAgB,CAAC;AAI9E,cAAM,QAAW,mBAAgB,CAAC,GAAG,aAAa,CAAC,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC,GAAG,SAAS;AACvF,gBAAM,UAAU,SAAS;AACzB,cAAI,EAAE,QAAQ,cAAc,WAAW,QAAQ;AAC/C,cAAI,WAAW,mBAAmB;AAChC,mBAAO;AAAA,UACT;AACA,gBAAM,4BAA4B,QAAQ,qBAAqB,QAAQ,KAAK,WAAW;AAKvF,cAAI,gBAAgB,2BAA2B;AAC7C,gBAAI,8BAA8B,QAAQ,mBAAmB,WAAW,GAAG;AACzE,sBAAQ,KAAK,uFAAuF,gBAAgB,KAAK,UAAU,OAAO,CAAC;AAC3I,qBAAO;AAAA,YACT;AACA,4BAAgB,eAAe,2BAA2B;AAAA,UAC5D;AACA,iBAAO,IAAK,SAAS,eAAe;AAAA,QACtC,GAAG,CAAC;AACJ,eAAO,WAAW,KAAK;AAAA,MACzB;AAAA,IACF;AAAA,IACA,uBAAwB,OAAO,SAAS;AACtC,aAAO,CAAC,gBAAgB;AACtB,cAAM,iBAAiB,QAAQ,oBAAoB;AACnD,YAAI,gBAAgB;AAClB,cAAI,SAAS,CAAC;AACd,gBAAM,EAAE,iBAAiB;AACzB,qBAAW,YAAY,cAAc;AACnC,uBAAW,UAAU,aAAa,WAAW;AAC3C,uBAAS,OAAO,OAAO,aAAa,UAAU,OAAO;AAAA,YACvD;AAAA,UACF;AACA,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,IACA,uBAAwB,OAAO,SAAS;AACtC,aAAO,OAAO,KAAK,QAAQ,aAAa;AAAA,IAC1C;AAAA,IACA,kBAAmB,OAAO,SAAS;AACjC,aAAO,QAAQ,uBAAuB;AAAA,IACxC;AAAA,IACA,oBAAqB,OAAO,SAAS;AACnC,YAAM,UAAU,QAAQ,kBAAkB;AAC1C,YAAM,iBAAiB,CAAC;AACxB,iBAAW,YAAY,SAAS;AAC9B,cAAM,SAAS,QAAQ;AACvB,YACE,OAAO,WAAW,cAAc,SAChC,OAAO,YAAY,wBACnB;AACA,yBAAe,QAAQ,UAAU,WAAW;AAAA,YAC1C,WAAW,QAAQ,UAAU;AAAA,YAC7B,SAAS,OAAO;AAAA,UAClB;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IACA,mBAAoB,OAAO,SAAS;AAClC,aAAO,QAAQ,qBAAqB;AAAA,IACtC;AAAA,IACA,sBAAuB,OAAO,SAAS;AACrC,aAAO,CAAC,gBAAe,qBAAqB;AAC1C,eAAO,QAAQ,cAAc,UAAU;AAAA,MACzC;AAAA,IACF;AAAA,IACA,cAAe,OAAO,SAAS;AAC7B,YAAM,kBAAkB,QAAQ;AAChC,aAAO,mBAAmB,mBAAW;AAAA,IACvC;AAAA,IACA,sBAAuB,OAAO,SAAS;AACrC,aAAO,QAAQ,oBAAoB,QAAQ,kBAAkB;AAAA,IAC/D;AAAA,IACA,2BAA4B,OAAO,SAAS;AAC1C,aAAO,QAAQ,eAAe;AAAA,IAChC;AAAA,IACA,oBAAqB,OAAO,SAAiB;AAE3C,aAAO,QAAQ,kBAAkB,oBAAoB,CAAC;AAAA,IACxD;AAAA,IACA,kBAAmB,OAAO,SAAS;AAKjC,aAAO,QAAQ,eAAe;AAAA,IAChC;AAAA,IACA,aAAc,OAAO,SAAS;AAC5B,aAAO,QAAQ,kBAAkB;AAAA,IACnC;AAAA,IACA,kBAAmB,OAAO,SAAS;AACjC,aAAO,QAAQ,kBAAkB;AAAA,IACnC;AAAA,IAIA,uBAAwB,OAAO,SAAS;AACtC,aAAO,CAAC,kBAA0B;AAGhC,cAAM,gBAAgB,QAAQ;AAC9B,cAAM,YAAY,CAAC;AACnB,mBAAW,YAAY,eAAe;AACpC,gBAAM,EAAE,mBAAmB,eAAe,cAAc;AACxD,cAAI,mBAAmB;AACrB,kBAAM,SAAS,cAAc,UAAU;AACvC,kBAAM,WAAW,sBAAsB,iBAAiB,SAAS,QAAQ,qBAAqB;AAE9F,gBAAI,OAAO,oBAAoB,aAAa,EAAE,YAAY;AAC1D,gBAAI,mBAAmB;AAAA,cACrB,MAAM;AAAA,cACN,aAAa;AAAA,cACb,cAAc,QAAQ,cAAc;AAAA,YACtC,CAAC,GAAG;AACF,qBAAO;AAAA,YACT;AACA,sBAAU,KAAK,EAAE,MAAM,UAAU,UAAU,KAAK,CAAC;AAAA,UACnD;AAAA,QACF;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,kBAAmB,OAAO,SAAS;AACjC,aAAO,CAAC,gBAAgB;AACtB,cAAM,SAAS,QAAQ,uBAAuB,WAAW;AACzD,cAAM,SAAS,CAAC;AAChB,YAAI,UAAU,OAAO,SAAS,GAAG;AAC/B,gBAAM,WAAW,QAAQ,kBAAkB;AAC3C,qBAAW,eAAe,QAAQ;AAChC,kBAAM,UAAU,SAAS;AACzB,gBAAI,QAAQ,KAAK,WAAW,mBAAmB;AAC7C,qBAAO,KAAK;AAAA,gBACV,MAAM,QAAQ,KAAK;AAAA,gBACnB,IAAI,QAAQ,KAAK;AAAA,gBACjB,MAAM;AAAA,gBACN,QAAQ,QAAQ,KAAK;AAAA,gBACrB,QAAQ,CAAC,CAAC,QAAQ,KAAK;AAAA,gBACvB,MAAM,QAAQ,KAAK;AAAA,cACrB,CAAC;AAAA,YACH;AAAA,UACF;AAAA,QACF;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EAWF;AAAA,EAGA,SAAS;AAAA,IAEP,sBAAsB;AAAA,MACpB,UAAU,cAAc;AAAA,QACtB,SAAS,MAAM,QAAQ,UAAU;AAAA,QACjC,UAAU,cAAc;AAAA,UAEtB,WAAW;AAAA,UACX,cAAc;AAAA,UACd,cAAc;AAAA,UACd,eAAe;AAAA,UACf,iBAAiB;AAAA,UACjB,kBAAkB;AAAA,UAClB,0BAA0B;AAAA,UAC1B,sBAAsB;AAAA,UACtB,WAAW,SAAS;AAAA,YAClB,CAAC,yBAAyB;AAAA,YAC1B,CAAC,yBAAyB;AAAA,YAC1B,CAAC,gCAAgC;AAAA,YACjC,CAAC,mCAAmC;AAAA,YACpC,CAAC,mBAAmB;AAAA,UACtB,CAAC;AAAA,QACH,CAAC;AAAA,MACH,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,QAAQ,EAAE,OAAO,WAAW;AAE3C,cAAM,eAAe,MAAM;AAAA,UACzB,UAAU,CAAC;AAAA,UACX,kBAAkB,CAAC;AAAA,UACnB,SAAS,CAAC;AAAA,UACV,WAAW,CAAC;AAAA,UACZ,UAAU;AAAA,YACR,cAAc,KAAK;AAAA,YACnB,0BAA0B,KAAK;AAAA,YAC/B,wBAAwB,uBAAuB;AAAA,YAC/C,sBAAsB,uBAAuB;AAAA,UAC/C;AAAA,UACA,UAAU;AAAA,YACR,CAAC,KAAK,WAAW,iBAAiB,KAAK,oBAAoB,KAAK,WAAW;AAAA,UAC7E;AAAA,UACA,WAAW,CAAC;AAAA,QACd,GAAG,IAAI;AACP,mBAAW,OAAO,cAAc;AAC9B,mBAAI,IAAI,OAAO,KAAK,aAAa,IAAI;AAAA,QACvC;AACA,gCAAwB,EAAE,MAAM,OAAO,QAAQ,CAAC;AAAA,MAClD;AAAA,IACF;AAAA,IACA,8BAA8B;AAAA,MAC5B,UAAU,cAAc;AAAA,QAGtB,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR,gBAAgB,QAAQ,QAAQ,MAAM;AAAA,QAKtC,cAAc;AAAA,QACd,MAAM;AAAA,QACN,QAAQ;AAAA,QACR;AAAA,QACA,SAAS,SAAS,MAAM;AAAA,QACxB,MAAM,SAAS,MAAM;AAAA,MACvB,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,MAAM,QAAQ,EAAE,OAAO,WAAW;AACjD,YAAI,KAAK,WAAW,mBAAmB;AACrC,kBAAQ,MAAM,oBAAoB,0CAA0C,EAAE,MAAM,MAAM,KAAK,CAAC;AAChG,gBAAM,IAAI,eAAO,oBAAoB,yCAAyC;AAAA,QAChF;AACA,iBAAI,IAAI,MAAM,UAAU,MAAM;AAAA,UAC5B,MAAM;AAAA,YACJ,GAAG;AAAA,YACH,cAAc,QAAQ;AAAA,UACxB;AAAA,UACA;AAAA,UACA,SAAS,CAAC,CAAC,KAAK,aAAa,IAAI,CAAC;AAAA,QACpC,CAAC;AACD,cAAM,EAAE,iBAAiB,wBAAwB,EAAE,MAAM,OAAO,QAAQ,CAAC;AACzE,cAAM,WAAW,eAAe,cAAc,KAAK,UAAU,CAAC,CAAC;AAC/D,cAAM,SAAS,eAAe,UAAU,KAAK,QAAQ,CAAC,CAAC;AACvD,eAAO,KAAK,IAAI;AAAA,MAElB;AAAA,IACF;AAAA,IACA,oCAAoC;AAAA,MAClC,UAAU,cAAc;AAAA,QACtB,aAAa;AAAA,QACb,mBAAmB,cAAc;AAAA,UAC/B,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,MAAM;AAAA,QACR,CAAC;AAAA,MACH,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,MAAM,QAAQ,EAAE,OAAO,WAAW;AAGjD,cAAM,UAAU,MAAM,SAAS,KAAK;AAGpC,YAAI,CAAC,SAAS;AACZ,kBAAQ,MAAM,6BAA6B,KAAK,eAAe,EAAE,MAAM,MAAM,KAAK,CAAC;AACnF,gBAAM,IAAI,eAAO,oBAAoB,wCAAwC;AAAA,QAC/E;AAEA,YAAI,KAAK,aAAa,QAAQ,KAAK,YAAY,KAAK,aAAa,QAAQ,KAAK,QAAQ;AACpF,kBAAQ,MAAM,+BAA+B,KAAK,eAAe,QAAQ,KAAK,eAAe,QAAQ,KAAK,YAAY,EAAE,MAAM,MAAM,KAAK,CAAC;AAC1I,gBAAM,IAAI,eAAO,oBAAoB,8BAA8B;AAAA,QACrE;AACA,gBAAQ,QAAQ,KAAK,CAAC,KAAK,aAAa,IAAI,CAAC;AAC7C,cAAM,QAAQ,MAAM,KAAK,iBAAiB;AAE1C,YAAI,KAAK,kBAAkB,WAAW,mBAAmB;AACvD,kBAAQ,KAAK,gBAAgB,KAAK;AAElC,gBAAM,oBAAoB,QAAQ,qBAAqB,KAAK,WAAW;AACvE,gBAAM,qBAAqB,QAAQ,qBAAqB,QAAQ,KAAK,WAAW;AAChF,cAAI,oBAAoB,mBAAmB,kBAAkB,IAAI,GAAG;AAClE,uCAA2B,EAAE,QAAQ,oBAAoB,QAAQ,CAAC;AAAA,UACpE,OAAO;AACL,sCAA0B,EAAE,MAAM,OAAO,QAAQ,CAAC;AAAA,UACpD;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,+BAA+B;AAAA,MAC7B,UAAU,CAAC,MAAM,EAAE,OAAO,WAAW;AACnC,iBAAS;AAAA,UACP;AAAA,UACA,cAAc;AAAA,UACd,YAAY;AAAA,UACZ,iBAAiB;AAAA,QACnB,CAAC,EAAE,IAAI;AAEP,cAAM,gBAAgB,KAAK,KAAK,cAAc,CAAC,QAAQ,CAAC;AAGxD,mBAAW,QAAQ,MAAM,WAAW;AAClC,gBAAM,OAAO,MAAM,UAAU;AAC7B,cAAI,KAAK,WAAW,eAAe,KAAK,KAAK,iBAAiB,KAAK,cAAc;AAC/E;AAAA,UACF;AAEA,cAAI,kBAAkB,KAAK,KAAK,KAAK,cAAc,CAAC,QAAQ,CAAC,GAAG,aAAa,GAAG;AAC9E,kBAAM,IAAI,UAAU,EAAE,sCAAsC,CAAC;AAAA,UAC/D;AAAA,QAGF;AAAA,MACF;AAAA,MACA,QAAS,EAAE,MAAM,MAAM,QAAQ,EAAE,SAAS;AACxC,iBAAI,IAAI,MAAM,WAAW,MAAM;AAAA,UAC7B;AAAA,UACA;AAAA,UACA,OAAO,EAAE,CAAC,KAAK,WAAW,SAAS;AAAA,UACnC,QAAQ;AAAA,UACR,SAAS;AAAA,QACX,CAAC;AAAA,MAIH;AAAA,MACA,WAAY,EAAE,YAAY,MAAM,QAAQ,EAAE,WAAW;AACnD,cAAM,EAAE,aAAa,eAAI,kBAAkB;AAC3C,cAAM,mBAAmB;AAAA,UACvB,CAAC,yBAAyB;AAAA,UAC1B,CAAC,yBAAyB;AAAA,UAC1B,CAAC,gCAAgC;AAAA,UACjC,CAAC,mCAAmC;AAAA,UACpC,CAAC,mBAAmB;AAAA,QACtB;AAEA,cAAM,YAAY,QAAQ,aAAa,SAAS,QAAQ;AAExD,YAAI,wBAAwB,MAAM,SAAS,GAAG;AAC5C,yBAAI,yBAAyB,gBAAgB;AAAA,YAC3C,SAAS;AAAA,YACT,SAAS,KAAK;AAAA,YACd,SAAS,iBAAiB,KAAK;AAAA,UACjC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,IACA,mCAAmC;AAAA,MACjC,UAAU,SAAS;AAAA,QACjB,cAAc;AAAA,QACd,MAAM;AAAA,QACN,aAAa,SAAS,QAAQ,QAAQ,MAAM,CAAC;AAAA,MAC/C,CAAC;AAAA,MACD,QAAS,SAAS,EAAE,SAAS;AAC3B,cAAM,EAAE,MAAM,MAAM,SAAS;AAC7B,cAAM,WAAW,MAAM,UAAU,KAAK;AACtC,YAAI,CAAC,UAAU;AAEb,kBAAQ,MAAM,iCAAiC,KAAK,iBAAiB,EAAE,MAAM,MAAM,KAAK,CAAC;AACzF,gBAAM,IAAI,eAAO,oBAAoB,wCAAwC;AAAA,QAC/E;AACA,iBAAI,IAAI,SAAS,OAAO,KAAK,UAAU,KAAK,IAAI;AAGhD,YAAI,IAAI,KAAK,KAAK,WAAW,EAAE,QAAQ,IAAI,SAAS,KAAK,iBAAiB;AACxE,kBAAQ,KAAK,2CAA2C,EAAE,UAAU,MAAM,KAAK,CAAC;AAEhF;AAAA,QACF;AAEA,cAAM,SAAS,cAAY,SAAS,KAAK,YAAY,OAAO,SAAS,KAAK,cAAc,SAAS,KAAK;AACtG,YAAI,WAAW,YAAY,WAAW,cAAc;AAElD,4BAAU,SAAS,KAAK,cAAc,QAAQ,OAAO,OAAO;AAC5D,mBAAI,IAAI,UAAU,cAAc,KAAK,WAAW;AAAA,QAClD;AAAA,MACF;AAAA,MACA,WAAY,EAAE,YAAY,MAAM,QAAQ,EAAE,OAAO,WAAW;AAC1D,cAAM,WAAW,MAAM,UAAU,KAAK;AACtC,cAAM,EAAE,aAAa,eAAI,kBAAkB;AAC3C,cAAM,YAAY,QAAQ,aAAa,SAAS,QAAQ;AAExD,YAAI,UAAU,cACZ,wBAAwB,MAAM,SAAS,GAAG;AAC1C,yBAAI,yBAAyB,mBAAmB;AAAA,YAC9C,SAAS;AAAA,YACT,SAAS,KAAK;AAAA,YACd,gBAAgB,SAAS;AAAA,UAC3B,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,IACA,qCAAqC;AAAA,MACnC,UAAU,SAAS;AAAA,QACjB,cAAc;AAAA,MAChB,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,MAAM,cAAc,EAAE,SAAS;AAC9C,cAAM,WAAW,MAAM,UAAU,KAAK;AACtC,YAAI,CAAC,UAAU;AAEb,kBAAQ,MAAM,mCAAmC,KAAK,iBAAiB,EAAE,MAAM,KAAK,CAAC;AACrF,gBAAM,IAAI,eAAO,oBAAoB,wCAAwC;AAAA,QAC/E,WAAW,SAAS,KAAK,aAAa,KAAK,UAAU;AACnD,kBAAQ,MAAM,4BAA4B,KAAK,2BAA2B,SAAS,KAAK,gBAAgB,KAAK,aAAa,EAAE,MAAM,KAAK,CAAC;AACxI,gBAAM,IAAI,eAAO,oBAAoB,kCAAkC;AAAA,QACzE;AACA,iBAAI,IAAI,UAAU,UAAU,gBAAgB;AAC5C,wBAAgB,EAAE,OAAO,cAAc,KAAK,cAAc,UAAU,WAAW,CAAC;AAAA,MAClF;AAAA,IACF;AAAA,IACA,mCAAmC;AAAA,MACjC,UAAU,CAAC,MAAM,EAAE,OAAO,SAAS,WAAW;AAC5C,iBAAS;AAAA,UACP,QAAQ;AAAA,UACR,QAAQ,SAAS,MAAM;AAAA,UAGvB,cAAc,SAAS,MAAM;AAAA,UAC7B,iBAAiB,SAAS,SAAS;AAAA,YACjC,QAAQ;AAAA,UACV,CAAC,CAAC;AAAA,QACJ,CAAC,EAAE,IAAI;AAEP,cAAM,iBAAiB,KAAK;AAC5B,cAAM,eAAe,QAAQ;AAE7B,YAAI,CAAC,MAAM,SAAS,iBAAiB;AACnC,gBAAM,IAAI,UAAU,EAAE,wBAAwB,CAAC;AAAA,QACjD;AACA,YAAI,iBAAiB,KAAK,mBAAmB,KAAK,UAAU;AAC1D,gBAAM,IAAI,UAAU,EAAE,yBAAyB,CAAC;AAAA,QAClD;AAEA,YAAI,eAAe,GAAG;AAGpB,cAAI,KAAK,aAAa,MAAM,SAAS,cAAc;AACjD,kBAAM,IAAI,UAAU,EAAE,4CAA4C,CAAC;AAAA,UACrE;AAAA,QACF,OAAO;AAEL,gBAAM,WAAW,MAAM,UAAU,KAAK;AACtC,cAAI,CAAC,UAAU;AAEb,kBAAM,IAAI,UAAU,EAAE,mDAAmD,CAAC;AAAA,UAC5E;AAEA,cAAI,CAAC,SAAS,WAAW,SAAS,QAAQ,WAAW,KAAK,gBAAgB,QAAQ;AAChF,kBAAM,IAAI,UAAU,EAAE,8BAA8B,CAAC;AAAA,UACvD;AAAA,QACF;AAAA,MACF;AAAA,MACA,QAAS,EAAE,MAAM,QAAQ,EAAE,OAAO,WAAW;AAC3C,qBACE,EAAE,UAAU,KAAK,QAAQ,UAAU,KAAK,YAAY,GACpD,EAAE,MAAM,OAAO,QAAQ,CACzB;AAAA,MACF;AAAA,MACA,WAAY,EAAE,MAAM,MAAM,cAAc,EAAE,OAAO,WAAW;AAC1D,cAAM,YAAY,eAAI,kBAAkB;AACxC,cAAM,YAAY,UAAU,aAAa,CAAC;AAC1C,cAAM,EAAE,aAAa,UAAU;AAE/B,YAAI,KAAK,WAAW,UAAU;AAG5B,cAAI,eAAI,sBAAsB,eAAe,GAAG;AAC9C;AAAA,UACF;AAEA,gBAAM,kBAAkB,OAAO,KAAK,SAAS,EAC1C,KAAK,SAAO,UAAU,KAAK,SAAS,wBACnC,QAAQ,cAAc,UAAU,KAAK,QAAQ,KAAK;AACtD,yBAAI,qBAAqB,wBAAwB,CAAC,CAAC;AACnD,yBAAI,qBAAqB,qBAAqB,eAAe;AAG7D,yBAAI,4BAA4B,UAAU,EAAE,MAAM,OAAK;AACrD,oBAAQ,MAAM,6BAA6B,EAAE,0BAA0B,eAAe,CAAC;AAAA,UACzF,CAAC;AAMD,yBAAI,4BAA4B,YAAY,CAAC,uCAAuC,CAAC,EAClF,KAAK,WAAY;AAChB,kBAAM,SAAS,eAAI,mBAAmB;AACtC,kBAAM,aAAa,OAAO,aAAa;AACvC,kBAAM,WAAW,kBAAkB,eAAe;AAClD,gBAAI,eAAe,WAAW,eAAe,UAAU;AACrD,qBAAO,KAAK,EAAE,MAAM,SAAS,CAAC,EAAE,MAAM,QAAQ,IAAI;AAAA,YACpD;AAAA,UACF,CAAC,EAAE,MAAM,OAAK;AACZ,oBAAQ,MAAM,6BAA6B,EAAE,oCAAoC,oCAAoC,CAAC;AAAA,UACxH,CAAC;AAAA,QAEL,OAAO;AACL,gBAAM,YAAY,QAAQ,aAAa,QAAQ;AAE/C,cAAI,wBAAwB,MAAM,SAAS,GAAG;AAC5C,kBAAM,0BAA0B,KAAK,WAAW,KAAK;AAErD,2BAAI,yBACF,0BAA0B,gBAAgB,kBAC1C;AAAA,cACE,SAAS;AAAA,cACT,UAAU,0BAA0B,KAAK,WAAW,KAAK;AAAA,YAC3D,CAAC;AAAA,UACL;AAAA,QAKF;AAAA,MAEF;AAAA,IACF;AAAA,IACA,sCAAsC;AAAA,MACpC,UAAU,cAAc;AAAA,QACtB,QAAQ;AAAA,MACV,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,MAAM,cAAc,EAAE,OAAO,WAAW;AACvD,qBACE,EAAE,UAAU,KAAK,UAAU,UAAU,KAAK,YAAY,GACtD,EAAE,MAAM,OAAO,QAAQ,CACzB;AAEA,uBAAI,qCAAqC,YACvC,CAAC,8CAA8C;AAAA,UAC7C;AAAA,UACA,MAAM,EAAE,QAAQ,KAAK,UAAU,QAAQ,KAAK,UAAU,GAAG;AAAA,UACzD;AAAA,QACF,CAAC,CACH;AAAA,MACF;AAAA,IACF;AAAA,IACA,6BAA6B;AAAA,MAC3B,UAAU;AAAA,MACV,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAClC,iBAAI,IAAI,MAAM,SAAS,KAAK,cAAc,IAAI;AAAA,MAChD;AAAA,IACF;AAAA,IACA,mCAAmC;AAAA,MACjC,UAAU,SAAS;AAAA,QACjB,cAAc;AAAA,MAChB,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAClC,gBAAQ,MAAM,iBAAiB,MAAM,MAAM,OAAO;AAClD,cAAM,SAAS,MAAM,QAAQ,KAAK;AAClC,YAAI,OAAO,WAAW,cAAc,OAAO;AACzC,kBAAQ,MAAM,4BAA4B,KAAK,gBAAgB,OAAO,QAAQ;AAC9E;AAAA,QACF;AACA,iBAAI,IAAI,OAAO,WAAW,KAAK,UAAU,IAAI;AAC7C,YAAI,OAAO,KAAK,OAAO,SAAS,EAAE,WAAW,OAAO,UAAU;AAC5D,iBAAO,SAAS,cAAc;AAAA,QAChC;AAKA,iBAAI,IAAI,MAAM,UAAU,KAAK,UAAU,iBAAiB,KAAK,oBAAoB,KAAK,WAAW,CAAC;AAAA,MAIpG;AAAA,MAMA,MAAM,WAAY,EAAE,MAAM,cAAc,EAAE,SAAS;AACjD,cAAM,EAAE,aAAa,eAAI,kBAAkB;AAC3C,cAAM,EAAE,WAAW,CAAC,MAAM;AAI1B,YAAI,KAAK,aAAa,SAAS,UAAU;AAGvC,qBAAW,QAAQ,UAAU;AAC3B,gBAAI,SAAS,SAAS,UAAU;AAC9B,oBAAM,eAAI,0BAA0B,SAAS,MAAM,UAAU;AAAA,YAC/D;AAAA,UACF;AAAA,QACF,OAAO;AACL,gBAAM,YAAY,SAAS,SAAS;AAGpC,gBAAM,eAAI,0BAA0B,KAAK,kBAAkB;AAE3D,cAAI,wBAAwB,MAAM,SAAS,GAAG;AAC5C,2BAAI,yBAAyB,gBAAgB;AAAA,cAC3C,SAAS;AAAA,cACT,UAAU,KAAK;AAAA,YACjB,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,mCAAmC;AAAA,MACjC,UAAU,CAAC,MAAM,EAAE,OAAO,WAAW;AACnC,iBAAS;AAAA,UACP,cAAc;AAAA,QAChB,CAAC,EAAE,IAAI;AAEP,YAAI,CAAC,MAAM,QAAQ,KAAK,eAAe;AACrC,gBAAM,IAAI,UAAU,EAAE,0BAA0B,CAAC;AAAA,QACnD;AAAA,MACF;AAAA,MACA,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAClC,cAAM,SAAS,MAAM,QAAQ,KAAK;AAClC,iBAAI,IAAI,QAAQ,UAAU,cAAc,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,IACA,qCAAqC;AAAA,MAGnC,UAAU,cAAc;AAAA,QACtB,WAAW,OAAK,OAAO,MAAM;AAAA,QAC7B,cAAc,OAAK,OAAO,MAAM;AAAA,QAChC,cAAc,OAAK,OAAO,MAAM;AAAA,QAChC,eAAe,OAAK,OAAO,MAAM,YAAY,IAAI;AAAA,QACjD,iBAAiB,OAAK,OAAO,MAAM;AAAA,MACrC,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAClC,mBAAW,OAAO,MAAM;AACtB,mBAAI,IAAI,MAAM,UAAU,KAAK,KAAK,IAAI;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,IACA,yCAAyC;AAAA,MACvC,UAAU,cAAc;AAAA,QACtB,mBAAmB,OAAK,CAAC,gBAAgB,cAAc,EAAE,SAAS,CAAC;AAAA,QACnE,cAAc,OAAK,OAAO,MAAM,YAAY,KAAK;AAAA,QACjD,cAAc,OAAK,OAAO,MAAM,YAAY,KAAK;AAAA,QACjD,gBAAgB;AAAA,QAChB,iBAAiB,SAAS;AAAA,UACxB,SAAS;AAAA,UACT,MAAM;AAAA,QACR,CAAC;AAAA,QACD,mBAAmB;AAAA,QACnB,gBAAgB,QACd,SAAS;AAAA,UACP,MAAM;AAAA,UACN,OAAO;AAAA,QACT,CAAC,CACH;AAAA,MACF,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,QAAQ,EAAE,OAAO,WAAW;AAC3C,cAAM,eAAe,MAAM,SAAS,KAAK;AACzC,cAAM,cAAc,aAAa;AACjC,mBAAW,OAAO,MAAM;AACtB,gBAAM,QAAQ,KAAK;AACnB,kBAAQ;AAAA,iBACD;AACH,0BAAY,KAAK,KAAK;AACtB;AAAA,iBACG;AACH,0BAAY,OAAO,YAAY,QAAQ,KAAK,GAAG,CAAC;AAChD;AAAA,iBACG;AACH,0BAAY,OAAO,YAAY,QAAQ,MAAM,OAAO,GAAG,GAAG,MAAM,IAAI;AACpE;AAAA;AAEA,uBAAI,IAAI,cAAc,KAAK,KAAK;AAAA;AAAA,QAEtC;AACA,YAAI,KAAK,mBAAmB;AAE1B,oCAA0B,EAAE,MAAM,OAAO,QAAQ,CAAC;AAAA,QACpD;AAAA,MACF;AAAA,IACF;AAAA,IACA,2CAA2C;AAAA,MACzC,UAAU,cAAc;AAAA,QACtB,UAAU,OAAK,CAAC,iBAAiB,iBAAiB,EAAE,SAAS,CAAC;AAAA,QAC9D,eAAe;AAAA,QACf,YAAY;AAAA,MACd,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAElC,YAAI,KAAK,YAAY,KAAK,eAAe;AACvC,qBAAW,oBAAoB,MAAM,SAAS,WAAW;AACvD,qBAAI,IAAI,MAAM,SAAS,UAAU,mBAAmB,QAAQ,KAAK,QAAQ;AACzE,qBAAI,IAAI,MAAM,SAAS,UAAU,kBAAkB,aAAa,KAAK,WAAW,aAAa,KAAK,aAAa;AAAA,UACjH;AAAA,QACF;AAAA,MAQF;AAAA,IACF;AAAA,IACA,kCAAkC;AAAA,MAChC,UAAU,SAAS;AAAA,QACjB,YAAY;AAAA,QACZ,YAAY;AAAA,MACd,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAClC,cAAM,EAAE,MAAM,MAAM,iBAAiB,KAAK;AAC1C,iBAAI,IAAI,MAAM,WAAW,KAAK,YAAY;AAAA,UACxC,SAAS,KAAK;AAAA,UACd;AAAA,UACA;AAAA,UACA;AAAA,UACA,aAAa;AAAA,UACb,OAAO,CAAC;AAAA,QACV,CAAC;AACD,YAAI,CAAC,MAAM,mBAAmB;AAC5B,mBAAI,IAAI,OAAO,qBAAqB,KAAK,UAAU;AAAA,QACrD;AAAA,MACF;AAAA,IACF;AAAA,IACA,qCAAqC;AAAA,MACnC,UAAU,CAAC,MAAM,EAAE,SAAS,WAAW;AACrC,iBAAS,EAAE,YAAY,OAAO,CAAC,EAAE,IAAI;AAErC,YAAI,QAAQ,aAAa,KAAK,YAAY,YAAY,KAAK,UAAU;AACnE,gBAAM,IAAI,UAAU,EAAE,8CAA8C,CAAC;AAAA,QACvE;AAAA,MACF;AAAA,MACA,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAClC,iBAAI,OAAO,MAAM,WAAW,KAAK,UAAU;AAAA,MAC7C;AAAA,IACF;AAAA,IACA,oCAAoC;AAAA,MAClC,UAAU,SAAS;AAAA,QACjB,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,cAAc;AAAA,MAChB,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAClC,iBAAI,IAAI,MAAM,UAAU,KAAK,aAAa,SACxC,MAAM,UAAU,KAAK,YAAY,MAAM,OAAO,OAAK,MAAM,KAAK,MAAM,CAAC;AAAA,MACzE;AAAA,MACA,MAAM,WAAY,EAAE,MAAM,QAAQ,EAAE,SAAS;AAC3C,cAAM,YAAY,eAAI,kBAAkB;AACxC,YAAI,KAAK,aAAa,UAAU,SAAS,YAAY,CAAC,eAAI,sBAAsB,eAAe,GAAG;AAChG,gBAAM,cAAc,KAAK,eACrB,EAAE,QAAQ,KAAK,OAAO,IACtB,EAAE,QAAQ,KAAK,QAAQ,UAAU,KAAK,SAAS;AACnD,gBAAM,eAAI,6BAA6B,EAAE,YAAY,KAAK,YAAY,MAAM,YAAY,CAAC;AAAA,QAC3F;AAAA,MACF;AAAA,IACF;AAAA,IACA,mCAAmC;AAAA,MACjC,UAAU,cAAc;AAAA,QACtB,UAAU;AAAA,QACV,YAAY;AAAA,MACd,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,QAAQ,EAAE,SAAS;AAClC,cAAM,WAAW,KAAK,YAAY,KAAK;AACvC,cAAM,UAAU,KAAK,YAAY,MAAM,KAAK,QAAQ;AAAA,MACtD;AAAA,MACA,MAAM,WAAY,EAAE,MAAM,QAAQ,EAAE,SAAS;AAC3C,cAAM,YAAY,eAAI,kBAAkB;AACxC,cAAM,WAAW,KAAK,YAAY,KAAK;AACvC,YAAI,aAAa,UAAU,SAAS,UAAU;AAC5C,cAAI,CAAC,eAAI,sBAAsB,eAAe,KAAK,eAAI,sBAAsB,wBAAwB,GAAG;AAGtG,2BAAI,sBAAsB,uBAAuB,KAAK,UAAU;AAChE,kBAAM,eAAI,0BAA0B,KAAK,UAAU;AACnD,2BAAI,sBAAsB,uBAAuB,MAAS;AAC1D,2BAAI,sBAAsB,0BAA0B,KAAK;AAAA,UAC3D;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,qCAAqC;AAAA,MACnC,UAAU,SAAS;AAAA,QACjB,YAAY;AAAA,QACZ,MAAM;AAAA,MACR,CAAC;AAAA,MACD,QAAS,EAAE,MAAM,QAAQ,EAAE,OAAO,WAAW;AAC3C,iBAAI,IAAI,MAAM,WAAW,KAAK,YAAY;AAAA,UACxC,GAAG,QAAQ,aAAa,KAAK;AAAA,UAC7B,MAAM,KAAK;AAAA,QACb,CAAC;AAAA,MACH;AAAA,IACF;AAAA,IACA,GAAkE;AAAA,MAChE,4CAA4C;AAAA,QAC1C,UAAU;AAAA,QACV,QAAS,EAAE,QAAQ,EAAE,OAAO,WAAW;AACrC,kBAAQ,cAAc,mBAAmB,kBAAkB,KAAK,WAAW;AAAA,QAC7E;AAAA,MACF;AAAA,MACA,wCAAwC;AAAA,QACtC,UAAU,SAAS,EAAE,WAAW,QAAQ,YAAY,SAAS,OAAO,EAAE,CAAC;AAAA,QACvE,QAAS,EAAE,QAAQ;AACjB,gBAAM,YAAY,eAAO,KAAK;AAC9B,cAAI,KAAK;AAAY;AACrB,cAAI,WAAW;AACb,kBAAM,IAAI,UAAU,oBAAoB;AAAA,UAC1C,OAAO;AACL,kBAAM,IAAI,MAAM,uBAAuB,KAAK,WAAW;AAAA,UACzD;AAAA,QACF;AAAA,QACA,WAAY,SAAS,EAAE,SAAS;AAC9B,cAAI,CAAC,QAAQ,KAAK;AAAY;AAC9B,yBAAI,gDAAgD;AAAA,YAClD,GAAG;AAAA,YACH,MAAM,KAAK,QAAQ,MAAM,CAAC,YAAY,CAAC;AAAA,UACzC,GAAG,KAAK;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,EAEF;AAAA,EAWA,SAAS;AAAA,IACP,sCAAsC,eAAgB,YAAY,cAAc,UAAU;AACxF,YAAM,EAAE,aAAa,eAAI,kBAAkB,EAAE;AAC7C,YAAM,MAAM,aAAa,YAAY;AACrC,YAAM,aAAY,MAAM,eAAI,sBAAsB,GAAG,KAAK,CAAC;AAE3D,iBAAU,QAAQ,CAAC,cAAc,QAAQ,CAAC;AAC1C,aAAO,WAAU,SAAS,wBAAwB;AAChD,mBAAU,IAAI;AAAA,MAChB;AACA,YAAM,eAAI,sBAAsB,KAAK,UAAS;AAC9C,qBAAI,yBAAyB,mBAAmB,CAAC,cAAc,QAAQ,CAAC;AAAA,IAC1E;AAAA,EACF;AACF,CAAC;", + "names": [] +} diff --git a/test/contracts/identity.js b/test/contracts/identity.js new file mode 100644 index 0000000000..f2213bfc0b --- /dev/null +++ b/test/contracts/identity.js @@ -0,0 +1,501 @@ +"use strict"; + +// node_modules/@sbp/sbp/dist/module.mjs +var selectors = {}; +var domains = {}; +var globalFilters = []; +var domainFilters = {}; +var selectorFilters = {}; +var unsafeSelectors = {}; +var DOMAIN_REGEX = /^[^/]+/; +function sbp(selector, ...data) { + const domain = domainFromSelector(selector); + if (!selectors[selector]) { + throw new Error(`SBP: selector not registered: ${selector}`); + } + for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) { + if (filters) { + for (const filter of filters) { + if (filter(domain, selector, data) === false) + return; + } + } + } + return selectors[selector].call(domains[domain].state, ...data); +} +function domainFromSelector(selector) { + const domainLookup = DOMAIN_REGEX.exec(selector); + if (domainLookup === null) { + throw new Error(`SBP: selector missing domain: ${selector}`); + } + return domainLookup[0]; +} +var SBP_BASE_SELECTORS = { + "sbp/selectors/register": function(sels) { + const registered = []; + for (const selector in sels) { + const domain = domainFromSelector(selector); + if (selectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`); + } else if (typeof sels[selector] === "function") { + if (unsafeSelectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`); + } + const fn = selectors[selector] = sels[selector]; + registered.push(selector); + if (!domains[domain]) { + domains[domain] = { state: {} }; + } + if (selector === `${domain}/_init`) { + fn.call(domains[domain].state); + } + } + } + return registered; + }, + "sbp/selectors/unregister": function(sels) { + for (const selector of sels) { + if (!unsafeSelectors[selector]) { + throw new Error(`SBP: can't unregister locked selector: ${selector}`); + } + delete selectors[selector]; + } + }, + "sbp/selectors/overwrite": function(sels) { + sbp("sbp/selectors/unregister", Object.keys(sels)); + return sbp("sbp/selectors/register", sels); + }, + "sbp/selectors/unsafe": function(sels) { + for (const selector of sels) { + if (selectors[selector]) { + throw new Error("unsafe must be called before registering selector"); + } + unsafeSelectors[selector] = true; + } + }, + "sbp/selectors/lock": function(sels) { + for (const selector of sels) { + delete unsafeSelectors[selector]; + } + }, + "sbp/selectors/fn": function(sel) { + return selectors[sel]; + }, + "sbp/filters/global/add": function(filter) { + globalFilters.push(filter); + }, + "sbp/filters/domain/add": function(domain, filter) { + if (!domainFilters[domain]) + domainFilters[domain] = []; + domainFilters[domain].push(filter); + }, + "sbp/filters/selector/add": function(selector, filter) { + if (!selectorFilters[selector]) + selectorFilters[selector] = []; + selectorFilters[selector].push(filter); + } +}; +SBP_BASE_SELECTORS["sbp/selectors/register"](SBP_BASE_SELECTORS); +var module_default = sbp; + +// frontend/common/common.js +import { default as default2 } from "vue"; + +// frontend/common/vSafeHtml.js +import dompurify from "dompurify"; +import Vue from "vue"; + +// frontend/model/contracts/shared/giLodash.js +function cloneDeep(obj) { + return JSON.parse(JSON.stringify(obj)); +} +function isMergeableObject(val) { + const nonNullObject = val && typeof val === "object"; + return nonNullObject && Object.prototype.toString.call(val) !== "[object RegExp]" && Object.prototype.toString.call(val) !== "[object Date]"; +} +function merge(obj, src) { + for (const key in src) { + const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : void 0; + if (clone && isMergeableObject(obj[key])) { + merge(obj[key], clone); + continue; + } + obj[key] = clone || src[key]; + } + return obj; +} + +// frontend/common/vSafeHtml.js +var defaultConfig = { + ALLOWED_ATTR: ["class"], + ALLOWED_TAGS: ["b", "br", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: true +}; +var transform = (el, binding) => { + if (binding.oldValue !== binding.value) { + let config = defaultConfig; + if (binding.arg === "a") { + config = cloneDeep(config); + config.ALLOWED_ATTR.push("href", "target"); + config.ALLOWED_TAGS.push("a"); + } + el.textContent = ""; + el.appendChild(dompurify.sanitize(binding.value, config)); + } +}; +Vue.directive("safe-html", { + bind: transform, + update: transform +}); + +// frontend/common/translations.js +import dompurify2 from "dompurify"; +import Vue2 from "vue"; + +// frontend/common/stringTemplate.js +var nargs = /\{([0-9a-zA-Z_]+)\}/g; +function template(string3, ...args) { + const firstArg = args[0]; + const replacementsByKey = typeof firstArg === "object" && firstArg !== null ? firstArg : args; + return string3.replace(nargs, function replaceArg(match, capture, index) { + if (string3[index - 1] === "{" && string3[index + match.length] === "}") { + return capture; + } + const maybeReplacement = Object.prototype.hasOwnProperty.call(replacementsByKey, capture) ? replacementsByKey[capture] : void 0; + if (maybeReplacement === null || maybeReplacement === void 0) { + return ""; + } + return String(maybeReplacement); + }); +} + +// frontend/common/translations.js +Vue2.prototype.L = L; +Vue2.prototype.LTags = LTags; +var defaultLanguage = "en-US"; +var defaultLanguageCode = "en"; +var defaultTranslationTable = {}; +var dompurifyConfig = { + ...defaultConfig, + ALLOWED_ATTR: ["class", "href", "rel", "target"], + ALLOWED_TAGS: ["a", "b", "br", "button", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: false +}; +var currentLanguage = defaultLanguage; +var currentLanguageCode = defaultLanguage.split("-")[0]; +var currentTranslationTable = defaultTranslationTable; +module_default("sbp/selectors/register", { + "translations/init": async function init(language) { + const [languageCode] = language.toLowerCase().split("-"); + if (language.toLowerCase() === currentLanguage.toLowerCase()) + return; + if (languageCode === currentLanguageCode) + return; + if (languageCode === defaultLanguageCode) { + currentLanguage = defaultLanguage; + currentLanguageCode = defaultLanguageCode; + currentTranslationTable = defaultTranslationTable; + return; + } + try { + currentTranslationTable = await module_default("backend/translations/get", language) || defaultTranslationTable; + currentLanguage = language; + currentLanguageCode = languageCode; + } catch (error) { + console.error(error); + } + } +}); +function LTags(...tags) { + const o = { + "br_": "
" + }; + for (const tag of tags) { + o[`${tag}_`] = `<${tag}>`; + o[`_${tag}`] = ``; + } + return o; +} +function L(key, args) { + return template(currentTranslationTable[key] || key, args).replace(/\s(?=[;:?!])/g, " "); +} +function sanitize(inputString) { + return dompurify2.sanitize(inputString, dompurifyConfig); +} +Vue2.component("i18n", { + functional: true, + props: { + args: [Object, Array], + tag: { + type: String, + default: "span" + }, + compile: Boolean + }, + render: function(h, context) { + const text = context.children[0].text; + const translation = L(text, context.props.args || {}); + if (!translation) { + console.warn("The following i18n text was not translated correctly:", text); + return h(context.props.tag, context.data, text); + } + if (context.props.tag === "a" && context.data.attrs.target === "_blank") { + context.data.attrs.rel = "noopener noreferrer"; + } + if (context.props.compile) { + const result = Vue2.compile("" + sanitize(translation) + ""); + return result.render.call({ + _c: (tag, ...args) => { + if (tag === "wrap") { + return h(context.props.tag, context.data, ...args); + } else { + return h(tag, ...args); + } + }, + _v: (x) => x + }); + } else { + if (!context.data.domProps) + context.data.domProps = {}; + context.data.domProps.innerHTML = sanitize(translation); + return h(context.props.tag, context.data); + } + } +}); + +// frontend/model/contracts/misc/flowTyper.js +var EMPTY_VALUE = Symbol("@@empty"); +var isEmpty = (v) => v === EMPTY_VALUE; +var isNil = (v) => v === null; +var isUndef = (v) => typeof v === "undefined"; +var isString = (v) => typeof v === "string"; +var isObject = (v) => !isNil(v) && typeof v === "object"; +var isFunction = (v) => typeof v === "function"; +var getType = (typeFn, _options) => { + if (isFunction(typeFn.type)) + return typeFn.type(_options); + return typeFn.name || "?"; +}; +var TypeValidatorError = class extends Error { + expectedType; + valueType; + value; + typeScope; + sourceFile; + constructor(message, expectedType, valueType, value, typeName = "", typeScope = "") { + const errMessage = message || `invalid "${valueType}" value type; ${typeName || expectedType} type expected`; + super(errMessage); + this.expectedType = expectedType; + this.valueType = valueType; + this.value = value; + this.typeScope = typeScope || ""; + this.sourceFile = this.getSourceFile(); + this.message = `${errMessage} +${this.getErrorInfo()}`; + this.name = this.constructor.name; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, TypeValidatorError); + } + } + getSourceFile() { + const fileNames = this.stack.match(/(\/[\w_\-.]+)+(\.\w+:\d+:\d+)/g) || []; + return fileNames.find((fileName) => fileName.indexOf("/flowTyper-js/dist/") === -1) || ""; + } + getErrorInfo() { + return ` + file ${this.sourceFile} + scope ${this.typeScope} + expected ${this.expectedType.replace(/\n/g, "")} + type ${this.valueType} + value ${this.value} +`; + } +}; +var validatorError = (typeFn, value, scope, message, expectedType, valueType) => { + return new TypeValidatorError(message, expectedType || getType(typeFn), valueType || typeof value, JSON.stringify(value), typeFn.name, scope); +}; +var arrayOf = (typeFn, _scope = "Array") => { + function array(value) { + if (isEmpty(value)) + return [typeFn(value)]; + if (Array.isArray(value)) { + let index = 0; + return value.map((v) => typeFn(v, `${_scope}[${index++}]`)); + } + throw validatorError(array, value, _scope); + } + array.type = () => `Array<${getType(typeFn)}>`; + return array; +}; +var object = function(value) { + if (isEmpty(value)) + return {}; + if (isObject(value) && !Array.isArray(value)) { + return Object.assign({}, value); + } + throw validatorError(object, value); +}; +var objectOf = (typeObj, _scope = "Object") => { + function object2(value) { + const o = object(value); + const typeAttrs = Object.keys(typeObj); + const unknownAttr = Object.keys(o).find((attr) => !typeAttrs.includes(attr)); + if (unknownAttr) { + throw validatorError(object2, value, _scope, `missing object property '${unknownAttr}' in ${_scope} type`); + } + const undefAttr = typeAttrs.find((property) => { + const propertyTypeFn = typeObj[property]; + return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + }); + if (undefAttr) { + throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); + } + const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { + const typeFn = typeObj[key]; + if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + return Object.assign(acc, {}); + } else { + return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); + } + }; + return typeAttrs.reduce(reducer, {}); + } + object2.type = () => { + const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + return `{| + ${props.join(",\n ")} +|}`; + }; + return object2; +}; +function objectMaybeOf(validations, _scope = "Object") { + return function(data) { + object(data); + for (const key in data) { + validations[key]?.(data[key], `${_scope}.${key}`); + } + return data; + }; +} +function undef(value, _scope = "") { + if (isEmpty(value) || isUndef(value)) + return void 0; + throw validatorError(undef, value, _scope); +} +undef.type = () => "void"; +var string = function string2(value, _scope = "") { + if (isEmpty(value)) + return ""; + if (isString(value)) + return value; + throw validatorError(string2, value, _scope); +}; + +// frontend/model/contracts/shared/validators.js +var allowedUsernameCharacters = (value) => /^[\w-]*$/.test(value); +var noConsecutiveHyphensOrUnderscores = (value) => !value.includes("--") && !value.includes("__"); +var noLeadingOrTrailingHyphen = (value) => !value.startsWith("-") && !value.endsWith("-"); +var noLeadingOrTrailingUnderscore = (value) => !value.startsWith("_") && !value.endsWith("_"); +var noUppercase = (value) => value.toLowerCase() === value; + +// frontend/model/contracts/shared/constants.js +var IDENTITY_USERNAME_MAX_CHARS = 80; + +// frontend/model/contracts/identity.js +module_default("chelonia/defineContract", { + name: "gi.contracts/identity", + getters: { + currentIdentityState(state) { + return state; + }, + loginState(state, getters) { + return getters.currentIdentityState.loginState; + } + }, + actions: { + "gi.contracts/identity": { + validate: (data, { state, meta }) => { + objectMaybeOf({ + attributes: objectMaybeOf({ + username: string, + email: string, + picture: string + }) + })(data); + const { username } = data.attributes; + if (username.length > IDENTITY_USERNAME_MAX_CHARS) { + throw new TypeError(`A username cannot exceed ${IDENTITY_USERNAME_MAX_CHARS} characters.`); + } + if (!allowedUsernameCharacters(username)) { + throw new TypeError("A username cannot contain disallowed characters."); + } + if (!noConsecutiveHyphensOrUnderscores(username)) { + throw new TypeError("A username cannot contain two consecutive hyphens or underscores."); + } + if (!noLeadingOrTrailingHyphen(username)) { + throw new TypeError("A username cannot start or end with a hyphen."); + } + if (!noLeadingOrTrailingUnderscore(username)) { + throw new TypeError("A username cannot start or end with an underscore."); + } + if (!noUppercase(username)) { + throw new TypeError("A username cannot contain uppercase letters."); + } + }, + process({ data }, { state }) { + const initialState = merge({ + settings: {}, + attributes: {} + }, data); + for (const key in initialState) { + default2.set(state, key, initialState[key]); + } + } + }, + "gi.contracts/identity/setAttributes": { + validate: object, + process({ data }, { state }) { + for (const key in data) { + default2.set(state.attributes, key, data[key]); + } + } + }, + "gi.contracts/identity/deleteAttributes": { + validate: arrayOf(string), + process({ data }, { state }) { + for (const attribute of data) { + default2.delete(state.attributes, attribute); + } + } + }, + "gi.contracts/identity/updateSettings": { + validate: object, + process({ data }, { state }) { + for (const key in data) { + default2.set(state.settings, key, data[key]); + } + } + }, + "gi.contracts/identity/setLoginState": { + validate: objectOf({ + groupIds: arrayOf(string) + }), + process({ data }, { state }) { + default2.set(state, "loginState", data); + }, + sideEffect({ contractID }) { + if (contractID === module_default("state/vuex/getters").ourIdentityContractId) { + module_default("chelonia/queueInvocation", contractID, ["gi.actions/identity/updateLoginStateUponLogin"]).catch((e) => { + module_default("gi.notifications/emit", "ERROR", { + message: L("Failed to join groups we're part of on another device. Not catastrophic, but could lead to problems. {errName}: '{errMsg}'", { + errName: e.name, + errMsg: e.message || "?" + }) + }); + }); + } + } + } + } +}); +//# sourceMappingURL=identity.js.map diff --git a/test/contracts/identity.js.map b/test/contracts/identity.js.map new file mode 100644 index 0000000000..55d720bdeb --- /dev/null +++ b/test/contracts/identity.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/validators.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/identity.js"], + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "// Matches strings of plain ASCII letters and digits, hyphens and underscores.\nexport const allowedUsernameCharacters = (value ) => /^[\\w-]*$/.test(value)\n\n// Matches non-empty strings of plain ASCII letters and digits.\nexport const alphanumeric = (value ) => /^[A-Za-z\\d]+$/.test(value)\n\nexport const noConsecutiveHyphensOrUnderscores = (value ) => !value.includes('--') && !value.includes('__')\n\nexport const noLeadingOrTrailingHyphen = (value ) => !value.startsWith('-') && !value.endsWith('-')\nexport const noLeadingOrTrailingUnderscore = (value ) => !value.startsWith('_') && !value.endsWith('_')\n\nexport const decimals = (digits ) => (value ) => Number.isInteger(value * Math.pow(10, digits))\n\nexport const noUppercase = (value ) => value.toLowerCase() === value\n\nexport const noWhitespace = (value ) => /^\\S+$/.test(value)\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\n\nimport { Vue, L } from '@common/common.js'\nimport { merge } from './shared/giLodash.js'\nimport { objectOf, objectMaybeOf, arrayOf, string, object } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n allowedUsernameCharacters,\n noConsecutiveHyphensOrUnderscores,\n noLeadingOrTrailingHyphen,\n noLeadingOrTrailingUnderscore,\n noUppercase\n} from './shared/validators.js'\n\nimport { IDENTITY_USERNAME_MAX_CHARS } from './shared/constants.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/identity',\n getters: {\n currentIdentityState (state) {\n return state\n },\n loginState (state, getters) {\n return getters.currentIdentityState.loginState\n }\n },\n actions: {\n 'gi.contracts/identity': {\n validate: (data, { state, meta }) => {\n objectMaybeOf({\n attributes: objectMaybeOf({\n username: string,\n email: string,\n picture: string\n })\n })(data)\n const { username } = data.attributes\n if (username.length > IDENTITY_USERNAME_MAX_CHARS) {\n throw new TypeError(`A username cannot exceed ${IDENTITY_USERNAME_MAX_CHARS} characters.`)\n }\n if (!allowedUsernameCharacters(username)) {\n throw new TypeError('A username cannot contain disallowed characters.')\n }\n if (!noConsecutiveHyphensOrUnderscores(username)) {\n throw new TypeError('A username cannot contain two consecutive hyphens or underscores.')\n }\n if (!noLeadingOrTrailingHyphen(username)) {\n throw new TypeError('A username cannot start or end with a hyphen.')\n }\n if (!noLeadingOrTrailingUnderscore(username)) {\n throw new TypeError('A username cannot start or end with an underscore.')\n }\n if (!noUppercase(username)) {\n throw new TypeError('A username cannot contain uppercase letters.')\n }\n },\n process ({ data }, { state }) {\n const initialState = merge({\n settings: {},\n attributes: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/identity/setAttributes': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.attributes, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/deleteAttributes': {\n validate: arrayOf(string),\n process ({ data }, { state }) {\n for (const attribute of data) {\n Vue.delete(state.attributes, attribute)\n }\n }\n },\n 'gi.contracts/identity/updateSettings': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/setLoginState': {\n validate: objectOf({\n groupIds: arrayOf(string)\n }),\n process ({ data }, { state }) {\n Vue.set(state, 'loginState', data)\n },\n sideEffect ({ contractID }) {\n // it only makes sense to call updateLoginStateUponLogin for ourselves\n if (contractID === sbp('state/vuex/getters').ourIdentityContractId) {\n // makes sure that updateLoginStateUponLogin gets run after the entire identity\n // state has been synced, this way we don't end up joining groups we've left, etc.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/updateLoginStateUponLogin'])\n .catch((e) => {\n sbp('gi.notifications/emit', 'ERROR', {\n message: L(\"Failed to join groups we're part of on another device. Not catastrophic, but could lead to problems. {errName}: '{errMsg}'\", {\n errName: e.name,\n errMsg: e.message || '?'\n })\n })\n })\n }\n }\n }\n }\n})\n"], + "mappings": "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", + "names": [] +} diff --git a/test/contracts/mailbox.js b/test/contracts/mailbox.js new file mode 100644 index 0000000000..6c0b51bdf7 --- /dev/null +++ b/test/contracts/mailbox.js @@ -0,0 +1,543 @@ +"use strict"; + +// node_modules/@sbp/sbp/dist/module.mjs +var selectors = {}; +var domains = {}; +var globalFilters = []; +var domainFilters = {}; +var selectorFilters = {}; +var unsafeSelectors = {}; +var DOMAIN_REGEX = /^[^/]+/; +function sbp(selector, ...data) { + const domain = domainFromSelector(selector); + if (!selectors[selector]) { + throw new Error(`SBP: selector not registered: ${selector}`); + } + for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) { + if (filters) { + for (const filter of filters) { + if (filter(domain, selector, data) === false) + return; + } + } + } + return selectors[selector].call(domains[domain].state, ...data); +} +function domainFromSelector(selector) { + const domainLookup = DOMAIN_REGEX.exec(selector); + if (domainLookup === null) { + throw new Error(`SBP: selector missing domain: ${selector}`); + } + return domainLookup[0]; +} +var SBP_BASE_SELECTORS = { + "sbp/selectors/register": function(sels) { + const registered = []; + for (const selector in sels) { + const domain = domainFromSelector(selector); + if (selectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`); + } else if (typeof sels[selector] === "function") { + if (unsafeSelectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`); + } + const fn = selectors[selector] = sels[selector]; + registered.push(selector); + if (!domains[domain]) { + domains[domain] = { state: {} }; + } + if (selector === `${domain}/_init`) { + fn.call(domains[domain].state); + } + } + } + return registered; + }, + "sbp/selectors/unregister": function(sels) { + for (const selector of sels) { + if (!unsafeSelectors[selector]) { + throw new Error(`SBP: can't unregister locked selector: ${selector}`); + } + delete selectors[selector]; + } + }, + "sbp/selectors/overwrite": function(sels) { + sbp("sbp/selectors/unregister", Object.keys(sels)); + return sbp("sbp/selectors/register", sels); + }, + "sbp/selectors/unsafe": function(sels) { + for (const selector of sels) { + if (selectors[selector]) { + throw new Error("unsafe must be called before registering selector"); + } + unsafeSelectors[selector] = true; + } + }, + "sbp/selectors/lock": function(sels) { + for (const selector of sels) { + delete unsafeSelectors[selector]; + } + }, + "sbp/selectors/fn": function(sel) { + return selectors[sel]; + }, + "sbp/filters/global/add": function(filter) { + globalFilters.push(filter); + }, + "sbp/filters/domain/add": function(domain, filter) { + if (!domainFilters[domain]) + domainFilters[domain] = []; + domainFilters[domain].push(filter); + }, + "sbp/filters/selector/add": function(selector, filter) { + if (!selectorFilters[selector]) + selectorFilters[selector] = []; + selectorFilters[selector].push(filter); + } +}; +SBP_BASE_SELECTORS["sbp/selectors/register"](SBP_BASE_SELECTORS); +var module_default = sbp; + +// frontend/common/common.js +import { default as default2 } from "vue"; + +// frontend/common/vSafeHtml.js +import dompurify from "dompurify"; +import Vue from "vue"; + +// frontend/model/contracts/shared/giLodash.js +function cloneDeep(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +// frontend/common/vSafeHtml.js +var defaultConfig = { + ALLOWED_ATTR: ["class"], + ALLOWED_TAGS: ["b", "br", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: true +}; +var transform = (el, binding) => { + if (binding.oldValue !== binding.value) { + let config = defaultConfig; + if (binding.arg === "a") { + config = cloneDeep(config); + config.ALLOWED_ATTR.push("href", "target"); + config.ALLOWED_TAGS.push("a"); + } + el.textContent = ""; + el.appendChild(dompurify.sanitize(binding.value, config)); + } +}; +Vue.directive("safe-html", { + bind: transform, + update: transform +}); + +// frontend/common/translations.js +import dompurify2 from "dompurify"; +import Vue2 from "vue"; + +// frontend/common/stringTemplate.js +var nargs = /\{([0-9a-zA-Z_]+)\}/g; +function template(string3, ...args) { + const firstArg = args[0]; + const replacementsByKey = typeof firstArg === "object" && firstArg !== null ? firstArg : args; + return string3.replace(nargs, function replaceArg(match, capture, index) { + if (string3[index - 1] === "{" && string3[index + match.length] === "}") { + return capture; + } + const maybeReplacement = Object.prototype.hasOwnProperty.call(replacementsByKey, capture) ? replacementsByKey[capture] : void 0; + if (maybeReplacement === null || maybeReplacement === void 0) { + return ""; + } + return String(maybeReplacement); + }); +} + +// frontend/common/translations.js +Vue2.prototype.L = L; +Vue2.prototype.LTags = LTags; +var defaultLanguage = "en-US"; +var defaultLanguageCode = "en"; +var defaultTranslationTable = {}; +var dompurifyConfig = { + ...defaultConfig, + ALLOWED_ATTR: ["class", "href", "rel", "target"], + ALLOWED_TAGS: ["a", "b", "br", "button", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: false +}; +var currentLanguage = defaultLanguage; +var currentLanguageCode = defaultLanguage.split("-")[0]; +var currentTranslationTable = defaultTranslationTable; +module_default("sbp/selectors/register", { + "translations/init": async function init(language) { + const [languageCode] = language.toLowerCase().split("-"); + if (language.toLowerCase() === currentLanguage.toLowerCase()) + return; + if (languageCode === currentLanguageCode) + return; + if (languageCode === defaultLanguageCode) { + currentLanguage = defaultLanguage; + currentLanguageCode = defaultLanguageCode; + currentTranslationTable = defaultTranslationTable; + return; + } + try { + currentTranslationTable = await module_default("backend/translations/get", language) || defaultTranslationTable; + currentLanguage = language; + currentLanguageCode = languageCode; + } catch (error) { + console.error(error); + } + } +}); +function LTags(...tags) { + const o = { + "br_": "
" + }; + for (const tag of tags) { + o[`${tag}_`] = `<${tag}>`; + o[`_${tag}`] = ``; + } + return o; +} +function L(key, args) { + return template(currentTranslationTable[key] || key, args).replace(/\s(?=[;:?!])/g, " "); +} +function sanitize(inputString) { + return dompurify2.sanitize(inputString, dompurifyConfig); +} +Vue2.component("i18n", { + functional: true, + props: { + args: [Object, Array], + tag: { + type: String, + default: "span" + }, + compile: Boolean + }, + render: function(h, context) { + const text = context.children[0].text; + const translation = L(text, context.props.args || {}); + if (!translation) { + console.warn("The following i18n text was not translated correctly:", text); + return h(context.props.tag, context.data, text); + } + if (context.props.tag === "a" && context.data.attrs.target === "_blank") { + context.data.attrs.rel = "noopener noreferrer"; + } + if (context.props.compile) { + const result = Vue2.compile("" + sanitize(translation) + ""); + return result.render.call({ + _c: (tag, ...args) => { + if (tag === "wrap") { + return h(context.props.tag, context.data, ...args); + } else { + return h(tag, ...args); + } + }, + _v: (x) => x + }); + } else { + if (!context.data.domProps) + context.data.domProps = {}; + context.data.domProps.innerHTML = sanitize(translation); + return h(context.props.tag, context.data); + } + } +}); + +// frontend/model/contracts/misc/flowTyper.js +var EMPTY_VALUE = Symbol("@@empty"); +var isEmpty = (v) => v === EMPTY_VALUE; +var isNil = (v) => v === null; +var isUndef = (v) => typeof v === "undefined"; +var isBoolean = (v) => typeof v === "boolean"; +var isNumber = (v) => typeof v === "number"; +var isString = (v) => typeof v === "string"; +var isObject = (v) => !isNil(v) && typeof v === "object"; +var isFunction = (v) => typeof v === "function"; +var getType = (typeFn, _options) => { + if (isFunction(typeFn.type)) + return typeFn.type(_options); + return typeFn.name || "?"; +}; +var TypeValidatorError = class extends Error { + expectedType; + valueType; + value; + typeScope; + sourceFile; + constructor(message, expectedType, valueType, value, typeName = "", typeScope = "") { + const errMessage = message || `invalid "${valueType}" value type; ${typeName || expectedType} type expected`; + super(errMessage); + this.expectedType = expectedType; + this.valueType = valueType; + this.value = value; + this.typeScope = typeScope || ""; + this.sourceFile = this.getSourceFile(); + this.message = `${errMessage} +${this.getErrorInfo()}`; + this.name = this.constructor.name; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, TypeValidatorError); + } + } + getSourceFile() { + const fileNames = this.stack.match(/(\/[\w_\-.]+)+(\.\w+:\d+:\d+)/g) || []; + return fileNames.find((fileName) => fileName.indexOf("/flowTyper-js/dist/") === -1) || ""; + } + getErrorInfo() { + return ` + file ${this.sourceFile} + scope ${this.typeScope} + expected ${this.expectedType.replace(/\n/g, "")} + type ${this.valueType} + value ${this.value} +`; + } +}; +var validatorError = (typeFn, value, scope, message, expectedType, valueType) => { + return new TypeValidatorError(message, expectedType || getType(typeFn), valueType || typeof value, JSON.stringify(value), typeFn.name, scope); +}; +var arrayOf = (typeFn, _scope = "Array") => { + function array(value) { + if (isEmpty(value)) + return [typeFn(value)]; + if (Array.isArray(value)) { + let index = 0; + return value.map((v) => typeFn(v, `${_scope}[${index++}]`)); + } + throw validatorError(array, value, _scope); + } + array.type = () => `Array<${getType(typeFn)}>`; + return array; +}; +var literalOf = (primitive) => { + function literal(value, _scope = "") { + if (isEmpty(value) || value === primitive) + return primitive; + throw validatorError(literal, value, _scope); + } + literal.type = () => { + if (isBoolean(primitive)) + return `${primitive ? "true" : "false"}`; + else + return `"${primitive}"`; + }; + return literal; +}; +var mapOf = (keyTypeFn, typeFn) => { + function mapOf2(value) { + if (isEmpty(value)) + return {}; + const o = object(value); + const reducer = (acc, key) => Object.assign(acc, { + [keyTypeFn(key, "Map[_]")]: typeFn(o[key], `Map.${key}`) + }); + return Object.keys(o).reduce(reducer, {}); + } + mapOf2.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`; + return mapOf2; +}; +var object = function(value) { + if (isEmpty(value)) + return {}; + if (isObject(value) && !Array.isArray(value)) { + return Object.assign({}, value); + } + throw validatorError(object, value); +}; +var objectOf = (typeObj, _scope = "Object") => { + function object2(value) { + const o = object(value); + const typeAttrs = Object.keys(typeObj); + const unknownAttr = Object.keys(o).find((attr) => !typeAttrs.includes(attr)); + if (unknownAttr) { + throw validatorError(object2, value, _scope, `missing object property '${unknownAttr}' in ${_scope} type`); + } + const undefAttr = typeAttrs.find((property) => { + const propertyTypeFn = typeObj[property]; + return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + }); + if (undefAttr) { + throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); + } + const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { + const typeFn = typeObj[key]; + if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + return Object.assign(acc, {}); + } else { + return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); + } + }; + return typeAttrs.reduce(reducer, {}); + } + object2.type = () => { + const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + return `{| + ${props.join(",\n ")} +|}`; + }; + return object2; +}; +function objectMaybeOf(validations, _scope = "Object") { + return function(data) { + object(data); + for (const key in data) { + validations[key]?.(data[key], `${_scope}.${key}`); + } + return data; + }; +} +var optional = (typeFn) => { + const unionFn = unionOf(typeFn, undef); + function optional2(v) { + return unionFn(v); + } + optional2.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn); + return optional2; +}; +function undef(value, _scope = "") { + if (isEmpty(value) || isUndef(value)) + return void 0; + throw validatorError(undef, value, _scope); +} +undef.type = () => "void"; +var number = function number2(value, _scope = "") { + if (isEmpty(value)) + return 0; + if (isNumber(value)) + return value; + throw validatorError(number2, value, _scope); +}; +var string = function string2(value, _scope = "") { + if (isEmpty(value)) + return ""; + if (isString(value)) + return value; + throw validatorError(string2, value, _scope); +}; +function unionOf_(...typeFuncs) { + function union(value, _scope = "") { + for (const typeFn of typeFuncs) { + try { + return typeFn(value, _scope); + } catch (_) { + } + } + throw validatorError(union, value, _scope); + } + union.type = () => `(${typeFuncs.map((fn) => getType(fn)).join(" | ")})`; + return union; +} +var unionOf = unionOf_; + +// frontend/model/contracts/shared/constants.js +var CHATROOM_TYPES = { + INDIVIDUAL: "individual", + GROUP: "group" +}; +var CHATROOM_PRIVACY_LEVEL = { + GROUP: "chatroom-privacy-level-group", + PRIVATE: "chatroom-privacy-level-private", + PUBLIC: "chatroom-privacy-level-public" +}; +var MESSAGE_TYPES = { + POLL: "message-poll", + TEXT: "message-text", + INTERACTIVE: "message-interactive", + NOTIFICATION: "message-notification" +}; +var MESSAGE_NOTIFICATIONS = { + ADD_MEMBER: "add-member", + JOIN_MEMBER: "join-member", + LEAVE_MEMBER: "leave-member", + KICK_MEMBER: "kick-member", + UPDATE_DESCRIPTION: "update-description", + UPDATE_NAME: "update-name", + DELETE_CHANNEL: "delete-channel", + VOTE: "vote" +}; +var MAIL_TYPE_MESSAGE = "message"; +var MAIL_TYPE_FRIEND_REQ = "friend-request"; + +// frontend/model/contracts/shared/types.js +var inviteType = objectOf({ + inviteSecret: string, + quantity: number, + creator: string, + invitee: optional(string), + status: string, + responses: mapOf(string, string), + expires: number +}); +var chatRoomAttributesType = objectOf({ + name: string, + description: string, + type: unionOf(...Object.values(CHATROOM_TYPES).map((v) => literalOf(v))), + privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map((v) => literalOf(v))) +}); +var messageType = objectMaybeOf({ + type: unionOf(...Object.values(MESSAGE_TYPES).map((v) => literalOf(v))), + text: string, + notification: objectMaybeOf({ + type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map((v) => literalOf(v))), + params: mapOf(string, string) + }), + replyingMessage: objectOf({ + id: string, + text: string + }), + emoticons: mapOf(string, arrayOf(string)), + onlyVisibleTo: arrayOf(string) +}); +var mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map((k) => literalOf(k))); + +// frontend/model/contracts/mailbox.js +module_default("chelonia/defineContract", { + name: "gi.contracts/mailbox", + metadata: { + validate: objectOf({ + createdDate: string + }), + create() { + return { + createdDate: new Date().toISOString() + }; + } + }, + actions: { + "gi.contracts/mailbox": { + validate: object, + process({ data }, { state }) { + for (const key in data) { + default2.set(state, key, data[key]); + } + default2.set(state, "messages", []); + } + }, + "gi.contracts/mailbox/postMessage": { + validate: objectOf({ + messageType: mailType, + from: string, + subject: optional(string), + message: optional(string), + headers: optional(object) + }), + process(message, { state }) { + state.messages.push(message); + } + }, + "gi.contracts/mailbox/authorizeSender": { + validate: objectOf({ + sender: string + }), + process({ data }, { state }) { + throw new Error("unimplemented!"); + } + } + } +}); +//# sourceMappingURL=mailbox.js.map diff --git a/test/contracts/mailbox.js.map b/test/contracts/mailbox.js.map new file mode 100644 index 0000000000..0b239b5707 --- /dev/null +++ b/test/contracts/mailbox.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/mailbox.js"], + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { mailType } from './shared/types.js'\nimport { objectOf, string, object, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/mailbox',\n metadata: {\n // TODO: why is this missing the from username..?\n validate: objectOf({\n createdDate: string\n }),\n create () {\n return {\n createdDate: new Date().toISOString()\n }\n }\n },\n actions: {\n 'gi.contracts/mailbox': {\n validate: object, // TODO: define this\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state, key, data[key])\n }\n Vue.set(state, 'messages', [])\n }\n },\n 'gi.contracts/mailbox/postMessage': {\n validate: objectOf({\n messageType: mailType,\n from: string,\n subject: optional(string),\n message: optional(string),\n headers: optional(object)\n }),\n process (message, { state }) {\n state.messages.push(message)\n }\n },\n 'gi.contracts/mailbox/authorizeSender': {\n validate: objectOf({\n sender: string\n }),\n process ({ data }, { state }) {\n // TODO: replace this via OP_KEY_*?\n throw new Error('unimplemented!')\n }\n }\n }\n})\n"], + "mappings": "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", + "names": [] +} diff --git a/test/contracts/misc/flowTyper.js b/test/contracts/misc/flowTyper.js new file mode 100644 index 0000000000..d5be469b74 --- /dev/null +++ b/test/contracts/misc/flowTyper.js @@ -0,0 +1,266 @@ +const EMPTY_VALUE = Symbol("@@empty"); +const isEmpty = (v) => v === EMPTY_VALUE; +const isNil = (v) => v === null; +const isUndef = (v) => typeof v === "undefined"; +const isBoolean = (v) => typeof v === "boolean"; +const isNumber = (v) => typeof v === "number"; +const isString = (v) => typeof v === "string"; +const isObject = (v) => !isNil(v) && typeof v === "object"; +const isFunction = (v) => typeof v === "function"; +const isType = (typeFn) => (v, _scope = "") => { + try { + typeFn(v, _scope); + return true; + } catch (_) { + return false; + } +}; +const typeOf = (schema) => schema(EMPTY_VALUE, ""); +const getType = (typeFn, _options) => { + if (isFunction(typeFn.type)) + return typeFn.type(_options); + return typeFn.name || "?"; +}; +class TypeValidatorError extends Error { + expectedType; + valueType; + value; + typeScope; + sourceFile; + constructor(message, expectedType, valueType, value, typeName = "", typeScope = "") { + const errMessage = message || `invalid "${valueType}" value type; ${typeName || expectedType} type expected`; + super(errMessage); + this.expectedType = expectedType; + this.valueType = valueType; + this.value = value; + this.typeScope = typeScope || ""; + this.sourceFile = this.getSourceFile(); + this.message = `${errMessage} +${this.getErrorInfo()}`; + this.name = this.constructor.name; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, TypeValidatorError); + } + } + getSourceFile() { + const fileNames = this.stack.match(/(\/[\w_\-.]+)+(\.\w+:\d+:\d+)/g) || []; + return fileNames.find((fileName) => fileName.indexOf("/flowTyper-js/dist/") === -1) || ""; + } + getErrorInfo() { + return ` + file ${this.sourceFile} + scope ${this.typeScope} + expected ${this.expectedType.replace(/\n/g, "")} + type ${this.valueType} + value ${this.value} +`; + } +} +const validatorError = (typeFn, value, scope, message, expectedType, valueType) => { + return new TypeValidatorError(message, expectedType || getType(typeFn), valueType || typeof value, JSON.stringify(value), typeFn.name, scope); +}; +const arrayOf = (typeFn, _scope = "Array") => { + function array(value) { + if (isEmpty(value)) + return [typeFn(value)]; + if (Array.isArray(value)) { + let index = 0; + return value.map((v) => typeFn(v, `${_scope}[${index++}]`)); + } + throw validatorError(array, value, _scope); + } + array.type = () => `Array<${getType(typeFn)}>`; + return array; +}; +const literalOf = (primitive) => { + function literal(value, _scope = "") { + if (isEmpty(value) || value === primitive) + return primitive; + throw validatorError(literal, value, _scope); + } + literal.type = () => { + if (isBoolean(primitive)) + return `${primitive ? "true" : "false"}`; + else + return `"${primitive}"`; + }; + return literal; +}; +const mapOf = (keyTypeFn, typeFn) => { + function mapOf2(value) { + if (isEmpty(value)) + return {}; + const o = object(value); + const reducer = (acc, key) => Object.assign(acc, { + [keyTypeFn(key, "Map[_]")]: typeFn(o[key], `Map.${key}`) + }); + return Object.keys(o).reduce(reducer, {}); + } + mapOf2.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`; + return mapOf2; +}; +const isPrimitiveFn = (typeName) => ["undefined", "null", "boolean", "number", "string"].includes(typeName); +const maybe = (typeFn) => { + function maybe2(value, _scope = "") { + return isNil(value) || isUndef(value) ? value : typeFn(value, _scope); + } + maybe2.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`; + return maybe2; +}; +const mixed = function mixed2(value) { + return value; +}; +const object = function(value) { + if (isEmpty(value)) + return {}; + if (isObject(value) && !Array.isArray(value)) { + return Object.assign({}, value); + } + throw validatorError(object, value); +}; +const objectOf = (typeObj, _scope = "Object") => { + function object2(value) { + const o = object(value); + const typeAttrs = Object.keys(typeObj); + const unknownAttr = Object.keys(o).find((attr) => !typeAttrs.includes(attr)); + if (unknownAttr) { + throw validatorError(object2, value, _scope, `missing object property '${unknownAttr}' in ${_scope} type`); + } + const undefAttr = typeAttrs.find((property) => { + const propertyTypeFn = typeObj[property]; + return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + }); + if (undefAttr) { + throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); + } + const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { + const typeFn = typeObj[key]; + if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + return Object.assign(acc, {}); + } else { + return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); + } + }; + return typeAttrs.reduce(reducer, {}); + } + object2.type = () => { + const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + return `{| + ${props.join(",\n ")} +|}`; + }; + return object2; +}; +function objectMaybeOf(validations, _scope = "Object") { + return function(data) { + object(data); + for (const key in data) { + validations[key]?.(data[key], `${_scope}.${key}`); + } + return data; + }; +} +const optional = (typeFn) => { + const unionFn = unionOf(typeFn, undef); + function optional2(v) { + return unionFn(v); + } + optional2.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn); + return optional2; +}; +const nil = function nil2(value) { + if (isEmpty(value) || isNil(value)) + return null; + throw validatorError(nil2, value); +}; +function undef(value, _scope = "") { + if (isEmpty(value) || isUndef(value)) + return void 0; + throw validatorError(undef, value, _scope); +} +undef.type = () => "void"; +const boolean = function boolean2(value, _scope = "") { + if (isEmpty(value)) + return false; + if (isBoolean(value)) + return value; + throw validatorError(boolean2, value, _scope); +}; +const number = function number2(value, _scope = "") { + if (isEmpty(value)) + return 0; + if (isNumber(value)) + return value; + throw validatorError(number2, value, _scope); +}; +const string = function string2(value, _scope = "") { + if (isEmpty(value)) + return ""; + if (isString(value)) + return value; + throw validatorError(string2, value, _scope); +}; +function tupleOf_(...typeFuncs) { + function tuple(value, _scope = "") { + const cardinality = typeFuncs.length; + if (isEmpty(value)) + return typeFuncs.map((fn) => fn(value)); + if (Array.isArray(value) && value.length === cardinality) { + const tupleValue = []; + for (let i = 0; i < cardinality; i += 1) { + tupleValue.push(typeFuncs[i](value[i], _scope)); + } + return tupleValue; + } + throw validatorError(tuple, value, _scope); + } + tuple.type = () => `[${typeFuncs.map((fn) => getType(fn)).join(", ")}]`; + return tuple; +} +const tupleOf = tupleOf_; +function unionOf_(...typeFuncs) { + function union(value, _scope = "") { + for (const typeFn of typeFuncs) { + try { + return typeFn(value, _scope); + } catch (_) { + } + } + throw validatorError(union, value, _scope); + } + union.type = () => `(${typeFuncs.map((fn) => getType(fn)).join(" | ")})`; + return union; +} +const unionOf = unionOf_; +export { + EMPTY_VALUE, + TypeValidatorError, + arrayOf, + boolean, + getType, + isBoolean, + isEmpty, + isFunction, + isNil, + isNumber, + isObject, + isString, + isType, + isUndef, + literalOf, + mapOf, + maybe, + mixed, + nil, + number, + object, + objectMaybeOf, + objectOf, + optional, + string, + tupleOf, + typeOf, + undef, + unionOf +}; +//# sourceMappingURL=flowTyper.js.map diff --git a/test/contracts/misc/flowTyper.js.map b/test/contracts/misc/flowTyper.js.map new file mode 100644 index 0000000000..517f1a10fd --- /dev/null +++ b/test/contracts/misc/flowTyper.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../../frontend/model/contracts/misc/flowTyper.js"], + "sourcesContent": ["// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n"], + "mappings": "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", + "names": [] +} diff --git a/test/contracts/shared/constants.js b/test/contracts/shared/constants.js new file mode 100644 index 0000000000..20df0d1347 --- /dev/null +++ b/test/contracts/shared/constants.js @@ -0,0 +1,113 @@ +"use strict"; + +// frontend/model/contracts/shared/constants.js +var IDENTITY_PASSWORD_MIN_CHARS = 7; +var IDENTITY_USERNAME_MAX_CHARS = 80; +var INVITE_INITIAL_CREATOR = "invite-initial-creator"; +var INVITE_STATUS = { + REVOKED: "revoked", + VALID: "valid", + USED: "used" +}; +var PROFILE_STATUS = { + ACTIVE: "active", + PENDING: "pending", + REMOVED: "removed" +}; +var PROPOSAL_RESULT = "proposal-result"; +var PROPOSAL_INVITE_MEMBER = "invite-member"; +var PROPOSAL_REMOVE_MEMBER = "remove-member"; +var PROPOSAL_GROUP_SETTING_CHANGE = "group-setting-change"; +var PROPOSAL_PROPOSAL_SETTING_CHANGE = "proposal-setting-change"; +var PROPOSAL_GENERIC = "generic"; +var PROPOSAL_ARCHIVED = "proposal-archived"; +var MAX_ARCHIVED_PROPOSALS = 100; +var STATUS_OPEN = "open"; +var STATUS_PASSED = "passed"; +var STATUS_FAILED = "failed"; +var STATUS_EXPIRED = "expired"; +var STATUS_CANCELLED = "cancelled"; +var CHATROOM_GENERAL_NAME = "General"; +var CHATROOM_NAME_LIMITS_IN_CHARS = 50; +var CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280; +var CHATROOM_ACTIONS_PER_PAGE = 40; +var CHATROOM_MESSAGES_PER_PAGE = 20; +var CHATROOM_MESSAGE_ACTION = "chatroom-message-action"; +var CHATROOM_DETAILS_UPDATED = "chatroom-details-updated"; +var MESSAGE_RECEIVE = "message-receive"; +var MESSAGE_SEND = "message-send"; +var CHATROOM_TYPES = { + INDIVIDUAL: "individual", + GROUP: "group" +}; +var CHATROOM_PRIVACY_LEVEL = { + GROUP: "chatroom-privacy-level-group", + PRIVATE: "chatroom-privacy-level-private", + PUBLIC: "chatroom-privacy-level-public" +}; +var MESSAGE_TYPES = { + POLL: "message-poll", + TEXT: "message-text", + INTERACTIVE: "message-interactive", + NOTIFICATION: "message-notification" +}; +var INVITE_EXPIRES_IN_DAYS = { + ON_BOARDING: 30, + PROPOSAL: 7 +}; +var MESSAGE_NOTIFICATIONS = { + ADD_MEMBER: "add-member", + JOIN_MEMBER: "join-member", + LEAVE_MEMBER: "leave-member", + KICK_MEMBER: "kick-member", + UPDATE_DESCRIPTION: "update-description", + UPDATE_NAME: "update-name", + DELETE_CHANNEL: "delete-channel", + VOTE: "vote" +}; +var MESSAGE_VARIANTS = { + PENDING: "pending", + SENT: "sent", + RECEIVED: "received", + FAILED: "failed" +}; +var MAIL_TYPE_MESSAGE = "message"; +var MAIL_TYPE_FRIEND_REQ = "friend-request"; +export { + CHATROOM_ACTIONS_PER_PAGE, + CHATROOM_DESCRIPTION_LIMITS_IN_CHARS, + CHATROOM_DETAILS_UPDATED, + CHATROOM_GENERAL_NAME, + CHATROOM_MESSAGES_PER_PAGE, + CHATROOM_MESSAGE_ACTION, + CHATROOM_NAME_LIMITS_IN_CHARS, + CHATROOM_PRIVACY_LEVEL, + CHATROOM_TYPES, + IDENTITY_PASSWORD_MIN_CHARS, + IDENTITY_USERNAME_MAX_CHARS, + INVITE_EXPIRES_IN_DAYS, + INVITE_INITIAL_CREATOR, + INVITE_STATUS, + MAIL_TYPE_FRIEND_REQ, + MAIL_TYPE_MESSAGE, + MAX_ARCHIVED_PROPOSALS, + MESSAGE_NOTIFICATIONS, + MESSAGE_RECEIVE, + MESSAGE_SEND, + MESSAGE_TYPES, + MESSAGE_VARIANTS, + PROFILE_STATUS, + PROPOSAL_ARCHIVED, + PROPOSAL_GENERIC, + PROPOSAL_GROUP_SETTING_CHANGE, + PROPOSAL_INVITE_MEMBER, + PROPOSAL_PROPOSAL_SETTING_CHANGE, + PROPOSAL_REMOVE_MEMBER, + PROPOSAL_RESULT, + STATUS_CANCELLED, + STATUS_EXPIRED, + STATUS_FAILED, + STATUS_OPEN, + STATUS_PASSED +}; +//# sourceMappingURL=constants.js.map diff --git a/test/contracts/shared/constants.js.map b/test/contracts/shared/constants.js.map new file mode 100644 index 0000000000..7bbb9a4afe --- /dev/null +++ b/test/contracts/shared/constants.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../../frontend/model/contracts/shared/constants.js"], + "sourcesContent": ["'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n"], + "mappings": ";;;AAIO,IAAM,8BAA8B;AACpC,IAAM,8BAA8B;AAIpC,IAAM,yBAAyB;AAC/B,IAAM,gBAAgB;AAAA,EAC3B,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AACR;AACO,IAAM,iBAAiB;AAAA,EAC5B,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AACX;AAEO,IAAM,kBAAkB;AACxB,IAAM,yBAAyB;AAC/B,IAAM,yBAAyB;AAC/B,IAAM,gCAAgC;AACtC,IAAM,mCAAmC;AACzC,IAAM,mBAAmB;AAEzB,IAAM,oBAAoB;AAC1B,IAAM,yBAAyB;AAE/B,IAAM,cAAc;AACpB,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AACtB,IAAM,iBAAiB;AACvB,IAAM,mBAAmB;AAIzB,IAAM,wBAAwB;AAC9B,IAAM,gCAAgC;AACtC,IAAM,uCAAuC;AAC7C,IAAM,4BAA4B;AAClC,IAAM,6BAA6B;AAGnC,IAAM,0BAA0B;AAChC,IAAM,2BAA2B;AACjC,IAAM,kBAAkB;AACxB,IAAM,eAAe;AAErB,IAAM,iBAAiB;AAAA,EAC5B,YAAY;AAAA,EACZ,OAAO;AACT;AAEO,IAAM,yBAAyB;AAAA,EACpC,OAAO;AAAA,EACP,SAAS;AAAA,EACT,QAAQ;AACV;AAEO,IAAM,gBAAgB;AAAA,EAC3B,MAAM;AAAA,EACN,MAAM;AAAA,EACN,aAAa;AAAA,EACb,cAAc;AAChB;AAEO,IAAM,yBAAyB;AAAA,EACpC,aAAa;AAAA,EACb,UAAU;AACZ;AAEO,IAAM,wBAAwB;AAAA,EACnC,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,MAAM;AACR;AAEO,IAAM,mBAAmB;AAAA,EAC9B,SAAS;AAAA,EACT,MAAM;AAAA,EACN,UAAU;AAAA,EACV,QAAQ;AACV;AAIO,IAAM,oBAAoB;AAC1B,IAAM,uBAAuB;", + "names": [] +} diff --git a/test/contracts/shared/functions.js b/test/contracts/shared/functions.js new file mode 100644 index 0000000000..56d57645a6 --- /dev/null +++ b/test/contracts/shared/functions.js @@ -0,0 +1,353 @@ +"use strict"; + +// node_modules/@sbp/sbp/dist/module.mjs +var selectors = {}; +var domains = {}; +var globalFilters = []; +var domainFilters = {}; +var selectorFilters = {}; +var unsafeSelectors = {}; +var DOMAIN_REGEX = /^[^/]+/; +function sbp(selector, ...data) { + const domain = domainFromSelector(selector); + if (!selectors[selector]) { + throw new Error(`SBP: selector not registered: ${selector}`); + } + for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) { + if (filters) { + for (const filter of filters) { + if (filter(domain, selector, data) === false) + return; + } + } + } + return selectors[selector].call(domains[domain].state, ...data); +} +function domainFromSelector(selector) { + const domainLookup = DOMAIN_REGEX.exec(selector); + if (domainLookup === null) { + throw new Error(`SBP: selector missing domain: ${selector}`); + } + return domainLookup[0]; +} +var SBP_BASE_SELECTORS = { + "sbp/selectors/register": function(sels) { + const registered = []; + for (const selector in sels) { + const domain = domainFromSelector(selector); + if (selectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`); + } else if (typeof sels[selector] === "function") { + if (unsafeSelectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`); + } + const fn = selectors[selector] = sels[selector]; + registered.push(selector); + if (!domains[domain]) { + domains[domain] = { state: {} }; + } + if (selector === `${domain}/_init`) { + fn.call(domains[domain].state); + } + } + } + return registered; + }, + "sbp/selectors/unregister": function(sels) { + for (const selector of sels) { + if (!unsafeSelectors[selector]) { + throw new Error(`SBP: can't unregister locked selector: ${selector}`); + } + delete selectors[selector]; + } + }, + "sbp/selectors/overwrite": function(sels) { + sbp("sbp/selectors/unregister", Object.keys(sels)); + return sbp("sbp/selectors/register", sels); + }, + "sbp/selectors/unsafe": function(sels) { + for (const selector of sels) { + if (selectors[selector]) { + throw new Error("unsafe must be called before registering selector"); + } + unsafeSelectors[selector] = true; + } + }, + "sbp/selectors/lock": function(sels) { + for (const selector of sels) { + delete unsafeSelectors[selector]; + } + }, + "sbp/selectors/fn": function(sel) { + return selectors[sel]; + }, + "sbp/filters/global/add": function(filter) { + globalFilters.push(filter); + }, + "sbp/filters/domain/add": function(domain, filter) { + if (!domainFilters[domain]) + domainFilters[domain] = []; + domainFilters[domain].push(filter); + }, + "sbp/filters/selector/add": function(selector, filter) { + if (!selectorFilters[selector]) + selectorFilters[selector] = []; + selectorFilters[selector].push(filter); + } +}; +SBP_BASE_SELECTORS["sbp/selectors/register"](SBP_BASE_SELECTORS); +var module_default = sbp; + +// frontend/model/contracts/shared/constants.js +var INVITE_STATUS = { + REVOKED: "revoked", + VALID: "valid", + USED: "used" +}; +var MESSAGE_TYPES = { + POLL: "message-poll", + TEXT: "message-text", + INTERACTIVE: "message-interactive", + NOTIFICATION: "message-notification" +}; + +// frontend/common/common.js +import { default as default2 } from "vue"; + +// frontend/common/vSafeHtml.js +import dompurify from "dompurify"; +import Vue from "vue"; + +// frontend/model/contracts/shared/giLodash.js +function cloneDeep(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +// frontend/common/vSafeHtml.js +var defaultConfig = { + ALLOWED_ATTR: ["class"], + ALLOWED_TAGS: ["b", "br", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: true +}; +var transform = (el, binding) => { + if (binding.oldValue !== binding.value) { + let config = defaultConfig; + if (binding.arg === "a") { + config = cloneDeep(config); + config.ALLOWED_ATTR.push("href", "target"); + config.ALLOWED_TAGS.push("a"); + } + el.textContent = ""; + el.appendChild(dompurify.sanitize(binding.value, config)); + } +}; +Vue.directive("safe-html", { + bind: transform, + update: transform +}); + +// frontend/common/translations.js +import dompurify2 from "dompurify"; +import Vue2 from "vue"; + +// frontend/common/stringTemplate.js +var nargs = /\{([0-9a-zA-Z_]+)\}/g; +function template(string, ...args) { + const firstArg = args[0]; + const replacementsByKey = typeof firstArg === "object" && firstArg !== null ? firstArg : args; + return string.replace(nargs, function replaceArg(match, capture, index) { + if (string[index - 1] === "{" && string[index + match.length] === "}") { + return capture; + } + const maybeReplacement = Object.prototype.hasOwnProperty.call(replacementsByKey, capture) ? replacementsByKey[capture] : void 0; + if (maybeReplacement === null || maybeReplacement === void 0) { + return ""; + } + return String(maybeReplacement); + }); +} + +// frontend/common/translations.js +Vue2.prototype.L = L; +Vue2.prototype.LTags = LTags; +var defaultLanguage = "en-US"; +var defaultLanguageCode = "en"; +var defaultTranslationTable = {}; +var dompurifyConfig = { + ...defaultConfig, + ALLOWED_ATTR: ["class", "href", "rel", "target"], + ALLOWED_TAGS: ["a", "b", "br", "button", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: false +}; +var currentLanguage = defaultLanguage; +var currentLanguageCode = defaultLanguage.split("-")[0]; +var currentTranslationTable = defaultTranslationTable; +module_default("sbp/selectors/register", { + "translations/init": async function init(language) { + const [languageCode] = language.toLowerCase().split("-"); + if (language.toLowerCase() === currentLanguage.toLowerCase()) + return; + if (languageCode === currentLanguageCode) + return; + if (languageCode === defaultLanguageCode) { + currentLanguage = defaultLanguage; + currentLanguageCode = defaultLanguageCode; + currentTranslationTable = defaultTranslationTable; + return; + } + try { + currentTranslationTable = await module_default("backend/translations/get", language) || defaultTranslationTable; + currentLanguage = language; + currentLanguageCode = languageCode; + } catch (error) { + console.error(error); + } + } +}); +function LTags(...tags) { + const o = { + "br_": "
" + }; + for (const tag of tags) { + o[`${tag}_`] = `<${tag}>`; + o[`_${tag}`] = ``; + } + return o; +} +function L(key, args) { + return template(currentTranslationTable[key] || key, args).replace(/\s(?=[;:?!])/g, " "); +} +function sanitize(inputString) { + return dompurify2.sanitize(inputString, dompurifyConfig); +} +Vue2.component("i18n", { + functional: true, + props: { + args: [Object, Array], + tag: { + type: String, + default: "span" + }, + compile: Boolean + }, + render: function(h, context) { + const text = context.children[0].text; + const translation = L(text, context.props.args || {}); + if (!translation) { + console.warn("The following i18n text was not translated correctly:", text); + return h(context.props.tag, context.data, text); + } + if (context.props.tag === "a" && context.data.attrs.target === "_blank") { + context.data.attrs.rel = "noopener noreferrer"; + } + if (context.props.compile) { + const result = Vue2.compile("" + sanitize(translation) + ""); + return result.render.call({ + _c: (tag, ...args) => { + if (tag === "wrap") { + return h(context.props.tag, context.data, ...args); + } else { + return h(tag, ...args); + } + }, + _v: (x) => x + }); + } else { + if (!context.data.domProps) + context.data.domProps = {}; + context.data.domProps.innerHTML = sanitize(translation); + return h(context.props.tag, context.data); + } + } +}); + +// frontend/model/contracts/shared/time.js +var MINS_MILLIS = 6e4; +var HOURS_MILLIS = 60 * MINS_MILLIS; +var DAYS_MILLIS = 24 * HOURS_MILLIS; +var MONTHS_MILLIS = 30 * DAYS_MILLIS; + +// frontend/views/utils/misc.js +function logExceptNavigationDuplicated(err) { + err.name !== "NavigationDuplicated" && console.error(err); +} + +// frontend/model/contracts/shared/functions.js +function createInvite({ quantity = 1, creator, expires, invitee }) { + return { + inviteSecret: `${parseInt(Math.random() * 1e4)}`, + quantity, + creator, + invitee, + status: INVITE_STATUS.VALID, + responses: {}, + expires: Date.now() + DAYS_MILLIS * expires + }; +} +function createMessage({ meta, data, hash, state }) { + const { type, text, replyingMessage } = data; + const { createdDate } = meta; + let newMessage = { + type, + datetime: new Date(createdDate).toISOString(), + id: hash, + from: meta.username + }; + if (type === MESSAGE_TYPES.TEXT) { + newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }; + } else if (type === MESSAGE_TYPES.POLL) { + } else if (type === MESSAGE_TYPES.NOTIFICATION) { + const params = { + channelName: state?.attributes.name, + channelDescription: state?.attributes.description, + ...data.notification + }; + delete params.type; + newMessage = { + ...newMessage, + notification: { type: data.notification.type, params } + }; + } else if (type === MESSAGE_TYPES.INTERACTIVE) { + } + return newMessage; +} +async function leaveChatRoom({ contractID }) { + const rootState = module_default("state/vuex/state"); + const rootGetters = module_default("state/vuex/getters"); + if (contractID === rootGetters.currentChatRoomId) { + module_default("state/vuex/commit", "setCurrentChatRoomId", { + groupId: rootState.currentGroupId + }); + const curRouteName = module_default("controller/router").history.current.name; + if (curRouteName === "GroupChat" || curRouteName === "GroupChatConversation") { + await module_default("controller/router").push({ name: "GroupChatConversation", params: { chatRoomId: rootGetters.currentChatRoomId } }).catch(logExceptNavigationDuplicated); + } + } + module_default("state/vuex/commit", "deleteChatRoomUnread", { chatRoomId: contractID }); + module_default("state/vuex/commit", "deleteChatRoomScrollPosition", { chatRoomId: contractID }); + module_default("chelonia/contract/remove", contractID).catch((e) => { + console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e); + }); +} +function findMessageIdx(id, messages) { + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].id === id) { + return i; + } + } + return -1; +} +function makeMentionFromUsername(username) { + return { + me: `@${username}`, + all: "@all" + }; +} +export { + createInvite, + createMessage, + findMessageIdx, + leaveChatRoom, + makeMentionFromUsername +}; +//# sourceMappingURL=functions.js.map diff --git a/test/contracts/shared/functions.js.map b/test/contracts/shared/functions.js.map new file mode 100644 index 0000000000..7df5c9f150 --- /dev/null +++ b/test/contracts/shared/functions.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../../node_modules/@sbp/sbp/dist/module.mjs", "../../../frontend/model/contracts/shared/constants.js", "../../../frontend/common/common.js", "../../../frontend/common/vSafeHtml.js", "../../../frontend/model/contracts/shared/giLodash.js", "../../../frontend/common/translations.js", "../../../frontend/common/stringTemplate.js", "../../../frontend/model/contracts/shared/time.js", "../../../frontend/views/utils/misc.js", "../../../frontend/model/contracts/shared/functions.js"], + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nexport function logExceptNavigationDuplicated (err ) {\n err.name !== 'NavigationDuplicated' && console.error(err)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { INVITE_STATUS, MESSAGE_TYPES } from './constants.js'\nimport { DAYS_MILLIS } from './time.js'\nimport { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js'\n\n// !!!!!!!!!!!!!!!\n// !! IMPORTANT !!\n// !!!!!!!!!!!!!!!\n//\n// DO NOT CHANGE THE LOGIC TO ANY OF THESE FUNCTIONS!\n// INSTEAD, CREATE NEW FUNCTIONS WITH DIFFERENT NAMES\n// AND USE THOSE INSTEAD!\n//\n// THIS IS A CONSEQUENCE OF SHARING THIS CODE WITH THE REST OF THE APP.\n// IF YOU DO NOT NEED TO SHARE CODE WITH THE REST OF THE APP (AND CAN\n// KEEP IT WITHIN THE CONTRACT ONLY), THEN YOU DON'T NEED TO WORRY ABOUT\n// THIS, AND SHOULD INCLUDE THOSE FUNCTIONS (WITHOUT EXPORTING THEM),\n// DIRECTLY IN YOUR CONTRACT DEFINITION FILE. THEN YOU CAN MODIFY\n// THEM AS MUCH AS YOU LIKE (and generate new contract versions out of them).\n\n// group.js related\n\nexport function createInvite ({ quantity = 1, creator, expires, invitee } \n \n ) \n \n \n \n \n \n \n \n {\n return {\n inviteSecret: `${parseInt(Math.random() * 10000)}`, // TODO: this\n quantity,\n creator,\n invitee,\n status: INVITE_STATUS.VALID,\n responses: {}, // { bob: true } list of usernames that accepted the invite.\n expires: Date.now() + DAYS_MILLIS * expires\n }\n}\n\n// chatroom.js related\n\nexport function createMessage ({ meta, data, hash, state } \n \n ) {\n const { type, text, replyingMessage } = data\n const { createdDate } = meta\n\n let newMessage = {\n type,\n datetime: new Date(createdDate).toISOString(),\n id: hash,\n from: meta.username\n }\n\n if (type === MESSAGE_TYPES.TEXT) {\n newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }\n } else if (type === MESSAGE_TYPES.POLL) {\n // TODO: Poll message creation\n } else if (type === MESSAGE_TYPES.NOTIFICATION) {\n const params = {\n channelName: state?.attributes.name,\n channelDescription: state?.attributes.description,\n ...data.notification\n }\n delete params.type\n newMessage = {\n ...newMessage,\n notification: { type: data.notification.type, params }\n }\n } else if (type === MESSAGE_TYPES.INTERACTIVE) {\n // TODO: Interactive message creation for proposals\n }\n return newMessage\n}\n\nexport async function leaveChatRoom ({ contractID } \n \n ) {\n const rootState = sbp('state/vuex/state')\n const rootGetters = sbp('state/vuex/getters')\n if (contractID === rootGetters.currentChatRoomId) {\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {\n groupId: rootState.currentGroupId\n })\n const curRouteName = sbp('controller/router').history.current.name\n if (curRouteName === 'GroupChat' || curRouteName === 'GroupChatConversation') {\n await sbp('controller/router')\n .push({ name: 'GroupChatConversation', params: { chatRoomId: rootGetters.currentChatRoomId } })\n .catch(logExceptNavigationDuplicated)\n }\n }\n\n sbp('state/vuex/commit', 'deleteChatRoomUnread', { chatRoomId: contractID })\n sbp('state/vuex/commit', 'deleteChatRoomScrollPosition', { chatRoomId: contractID })\n\n // NOTE: make sure *not* to await on this, since that can cause\n // a potential deadlock. See same warning in sideEffect for\n // 'gi.contracts/group/removeMember'\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e)\n })\n}\n\nexport function findMessageIdx (id , messages ) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].id === id) {\n return i\n }\n }\n return -1\n}\n\nexport function makeMentionFromUsername (username ) \n \n {\n return {\n me: `@${username}`,\n all: '@all'\n }\n}\n"], + "mappings": "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", + "names": [] +} diff --git a/test/contracts/shared/giLodash.js b/test/contracts/shared/giLodash.js new file mode 100644 index 0000000000..76d2910982 --- /dev/null +++ b/test/contracts/shared/giLodash.js @@ -0,0 +1,202 @@ +// frontend/model/contracts/shared/giLodash.js +function mapValues(obj, fn, o = {}) { + for (const key in obj) { + o[key] = fn(obj[key]); + } + return o; +} +function mapObject(obj, fn) { + return Object.fromEntries(Object.entries(obj).map(fn)); +} +function pick(o, props) { + const x = {}; + for (const k of props) { + x[k] = o[k]; + } + return x; +} +function pickWhere(o, where) { + const x = {}; + for (const k in o) { + if (where(o[k])) { + x[k] = o[k]; + } + } + return x; +} +function choose(array, indices) { + const x = []; + for (const idx of indices) { + x.push(array[idx]); + } + return x; +} +function omit(o, props) { + const x = {}; + for (const k in o) { + if (!props.includes(k)) { + x[k] = o[k]; + } + } + return x; +} +function cloneDeep(obj) { + return JSON.parse(JSON.stringify(obj)); +} +function isMergeableObject(val) { + const nonNullObject = val && typeof val === "object"; + return nonNullObject && Object.prototype.toString.call(val) !== "[object RegExp]" && Object.prototype.toString.call(val) !== "[object Date]"; +} +function merge(obj, src) { + for (const key in src) { + const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : void 0; + if (clone && isMergeableObject(obj[key])) { + merge(obj[key], clone); + continue; + } + obj[key] = clone || src[key]; + } + return obj; +} +function delay(msec) { + return new Promise((resolve, reject) => { + setTimeout(resolve, msec); + }); +} +function randomBytes(length) { + return crypto.getRandomValues(new Uint8Array(length)); +} +function randomHexString(length) { + return Array.from(randomBytes(length), (byte) => (byte % 16).toString(16)).join(""); +} +function randomIntFromRange(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +} +function randomFromArray(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} +function flatten(arr) { + let flat = []; + for (let i = 0; i < arr.length; i++) { + if (Array.isArray(arr[i])) { + flat = flat.concat(arr[i]); + } else { + flat.push(arr[i]); + } + } + return flat; +} +function zip() { + const arr = Array.prototype.slice.call(arguments); + const zipped = []; + let max = 0; + arr.forEach((current) => max = Math.max(max, current.length)); + for (const current of arr) { + for (let i = 0; i < max; i++) { + zipped[i] = zipped[i] || []; + zipped[i].push(current[i]); + } + } + return zipped; +} +function uniq(array) { + return Array.from(new Set(array)); +} +function union(...arrays) { + return uniq([].concat.apply([], arrays)); +} +function intersection(a1, ...arrays) { + return uniq(a1).filter((v1) => arrays.every((v2) => v2.indexOf(v1) >= 0)); +} +function difference(a1, ...arrays) { + const a2 = [].concat.apply([], arrays); + return a1.filter((v) => a2.indexOf(v) === -1); +} +function deepEqualJSONType(a, b) { + if (a === b) + return true; + if (a === null || b === null || typeof a !== typeof b) + return false; + if (typeof a !== "object") + return a === b; + if (Array.isArray(a)) { + if (a.length !== b.length) + return false; + } else if (a.constructor.name !== "Object") { + throw new Error(`not JSON type: ${a}`); + } + for (const key in a) { + if (!deepEqualJSONType(a[key], b[key])) + return false; + } + return true; +} +function debounce(func, wait, immediate) { + let timeout, args, context, timestamp, result; + if (wait == null) + wait = 100; + function later() { + const last = Date.now() - timestamp; + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + context = args = null; + } + } + } + const debounced = function() { + context = this; + args = arguments; + timestamp = Date.now(); + const callNow = immediate && !timeout; + if (!timeout) + timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + return result; + }; + debounced.clear = function() { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + }; + debounced.flush = function() { + if (timeout) { + result = func.apply(context, args); + context = args = null; + clearTimeout(timeout); + timeout = null; + } + }; + return debounced; +} +export { + choose, + cloneDeep, + debounce, + deepEqualJSONType, + delay, + difference, + flatten, + intersection, + mapObject, + mapValues, + merge, + omit, + pick, + pickWhere, + randomBytes, + randomFromArray, + randomHexString, + randomIntFromRange, + union, + uniq, + zip +}; +//# sourceMappingURL=giLodash.js.map diff --git a/test/contracts/shared/giLodash.js.map b/test/contracts/shared/giLodash.js.map new file mode 100644 index 0000000000..044fb2479c --- /dev/null +++ b/test/contracts/shared/giLodash.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../../frontend/model/contracts/shared/giLodash.js"], + "sourcesContent": ["// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n"], + "mappings": "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", + "names": [] +} diff --git a/test/contracts/shared/payments/index.js b/test/contracts/shared/payments/index.js new file mode 100644 index 0000000000..ea57acf6be --- /dev/null +++ b/test/contracts/shared/payments/index.js @@ -0,0 +1,110 @@ +"use strict"; + +// frontend/model/contracts/misc/flowTyper.js +var EMPTY_VALUE = Symbol("@@empty"); +var isEmpty = (v) => v === EMPTY_VALUE; +var isUndef = (v) => typeof v === "undefined"; +var isBoolean = (v) => typeof v === "boolean"; +var isFunction = (v) => typeof v === "function"; +var getType = (typeFn, _options) => { + if (isFunction(typeFn.type)) + return typeFn.type(_options); + return typeFn.name || "?"; +}; +var TypeValidatorError = class extends Error { + expectedType; + valueType; + value; + typeScope; + sourceFile; + constructor(message, expectedType, valueType, value, typeName = "", typeScope = "") { + const errMessage = message || `invalid "${valueType}" value type; ${typeName || expectedType} type expected`; + super(errMessage); + this.expectedType = expectedType; + this.valueType = valueType; + this.value = value; + this.typeScope = typeScope || ""; + this.sourceFile = this.getSourceFile(); + this.message = `${errMessage} +${this.getErrorInfo()}`; + this.name = this.constructor.name; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, TypeValidatorError); + } + } + getSourceFile() { + const fileNames = this.stack.match(/(\/[\w_\-.]+)+(\.\w+:\d+:\d+)/g) || []; + return fileNames.find((fileName) => fileName.indexOf("/flowTyper-js/dist/") === -1) || ""; + } + getErrorInfo() { + return ` + file ${this.sourceFile} + scope ${this.typeScope} + expected ${this.expectedType.replace(/\n/g, "")} + type ${this.valueType} + value ${this.value} +`; + } +}; +var validatorError = (typeFn, value, scope, message, expectedType, valueType) => { + return new TypeValidatorError(message, expectedType || getType(typeFn), valueType || typeof value, JSON.stringify(value), typeFn.name, scope); +}; +var literalOf = (primitive) => { + function literal(value, _scope = "") { + if (isEmpty(value) || value === primitive) + return primitive; + throw validatorError(literal, value, _scope); + } + literal.type = () => { + if (isBoolean(primitive)) + return `${primitive ? "true" : "false"}`; + else + return `"${primitive}"`; + }; + return literal; +}; +function undef(value, _scope = "") { + if (isEmpty(value) || isUndef(value)) + return void 0; + throw validatorError(undef, value, _scope); +} +undef.type = () => "void"; +function unionOf_(...typeFuncs) { + function union(value, _scope = "") { + for (const typeFn of typeFuncs) { + try { + return typeFn(value, _scope); + } catch (_) { + } + } + throw validatorError(union, value, _scope); + } + union.type = () => `(${typeFuncs.map((fn) => getType(fn)).join(" | ")})`; + return union; +} +var unionOf = unionOf_; + +// frontend/model/contracts/shared/payments/index.js +var PAYMENT_PENDING = "pending"; +var PAYMENT_CANCELLED = "cancelled"; +var PAYMENT_ERROR = "error"; +var PAYMENT_NOT_RECEIVED = "not-received"; +var PAYMENT_COMPLETED = "completed"; +var paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map((k) => literalOf(k))); +var PAYMENT_TYPE_MANUAL = "manual"; +var PAYMENT_TYPE_BITCOIN = "bitcoin"; +var PAYMENT_TYPE_PAYPAL = "paypal"; +var paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map((k) => literalOf(k))); +export { + PAYMENT_CANCELLED, + PAYMENT_COMPLETED, + PAYMENT_ERROR, + PAYMENT_NOT_RECEIVED, + PAYMENT_PENDING, + PAYMENT_TYPE_BITCOIN, + PAYMENT_TYPE_MANUAL, + PAYMENT_TYPE_PAYPAL, + paymentStatusType, + paymentType +}; +//# sourceMappingURL=index.js.map diff --git a/test/contracts/shared/payments/index.js.map b/test/contracts/shared/payments/index.js.map new file mode 100644 index 0000000000..114ff938c7 --- /dev/null +++ b/test/contracts/shared/payments/index.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../../../frontend/model/contracts/misc/flowTyper.js", "../../../../frontend/model/contracts/shared/payments/index.js"], + "sourcesContent": ["// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n"], + "mappings": ";;;AAmDO,IAAM,cAAc,OAAO,SAAS;AACpC,IAAM,UAAU,OAAK,MAAM;AAE3B,IAAM,UAAU,OAAK,OAAO,MAAM;AAClC,IAAM,YAAY,OAAK,OAAO,MAAM;AAIpC,IAAM,aAAa,OAAK,OAAO,MAAM;AAcrC,IAAM,UAAU,CAAC,QAAQ,aAAa;AAC3C,MAAI,WAAW,OAAO,IAAI;AAAG,WAAO,OAAO,KAAK,QAAQ;AACxD,SAAO,OAAO,QAAQ;AACxB;AAGO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YACE,SACA,cACA,WACA,OACA,WAAmB,IACnB,YAAqB,IACrB;AACA,UAAM,aAAa,WACjB,YAAY,0BAA0B,YAAY;AACpD,UAAM,UAAU;AAChB,SAAK,eAAe;AACpB,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,YAAY,aAAa;AAC9B,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,UAAU,GAAG;AAAA,EAAe,KAAK,aAAa;AACnD,SAAK,OAAO,KAAK,YAAY;AAC7B,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,kBAAkB;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,gBAAyB;AACvB,UAAM,YAAY,KAAK,MAAM,MAAM,gCAAgC,KAAK,CAAC;AACzE,WAAO,UAAU,KAAK,cAAY,SAAS,QAAQ,qBAAqB,MAAM,EAAE,KAAK;AAAA,EACvF;AAAA,EAEA,eAAwB;AACtB,WAAO;AAAA,eACI,KAAK;AAAA,eACL,KAAK;AAAA,eACL,KAAK,aAAa,QAAQ,OAAO,EAAE;AAAA,eACnC,KAAK;AAAA,eACL,KAAK;AAAA;AAAA,EAElB;AACF;AAKA,IAAM,iBAAoB,CACxB,QACA,OACA,OACA,SACA,cACA,cACuB;AACvB,SAAO,IAAI,mBACT,SACA,gBAAgB,QAAQ,MAAM,GAC9B,aAAa,OAAO,OACpB,KAAK,UAAU,KAAK,GACpB,OAAO,MACP,KACF;AACF;AAgBO,IAAM,YACM,CAAC,cAAmC;AACnD,mBAAkB,OAAO,SAAS,IAAI;AACpC,QAAI,QAAQ,KAAK,KAAM,UAAU;AAAY,aAAO;AACpD,UAAM,eAAe,SAAS,OAAO,MAAM;AAAA,EAC7C;AACA,UAAQ,OAAO,MAAM;AACnB,QAAI,UAAU,SAAS;AAAG,aAAO,GAAG,YAAY,SAAS;AAAA;AACpD,aAAO,IAAI;AAAA,EAClB;AACA,SAAO;AACT;AAoIK,eAAgB,OAAO,SAAS,IAAI;AACzC,MAAI,QAAQ,KAAK,KAAK,QAAQ,KAAK;AAAG,WAAO;AAC7C,QAAM,eAAe,OAAO,OAAO,MAAM;AAC3C;AACA,MAAM,OAAO,MAAM;AA4EnB,qBAAsB,WAAW;AAC/B,iBAAgB,OAAc,SAAS,IAAI;AACzC,eAAW,UAAU,WAAW;AAC9B,UAAI;AACF,eAAO,OAAO,OAAO,MAAM;AAAA,MAC7B,SAAS,GAAP;AAAA,MAAW;AAAA,IACf;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,IAAI,UAAU,IAAI,QAAM,QAAQ,EAAE,CAAC,EAAE,KAAK,KAAK;AAClE,SAAO;AACT;AAGO,IAAM,UAAU;;;ACzYhB,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAC1B,IAAM,gBAAgB;AACtB,IAAM,uBAAuB;AAC7B,IAAM,oBAAoB;AAC1B,IAAM,oBAA4B,QAAQ,GAAG,CAAC,iBAAiB,mBAAmB,eAAe,sBAAsB,iBAAiB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAChK,IAAM,sBAAsB;AAC5B,IAAM,uBAAuB;AAC7B,IAAM,sBAAsB;AAC5B,IAAM,cAAsB,QAAQ,GAAG,CAAC,qBAAqB,sBAAsB,mBAAmB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;", + "names": [] +} diff --git a/test/contracts/shared/voting/proposals.js b/test/contracts/shared/voting/proposals.js new file mode 100644 index 0000000000..4312b4f4d7 --- /dev/null +++ b/test/contracts/shared/voting/proposals.js @@ -0,0 +1,604 @@ +"use strict"; + +// node_modules/@sbp/sbp/dist/module.mjs +var selectors = {}; +var domains = {}; +var globalFilters = []; +var domainFilters = {}; +var selectorFilters = {}; +var unsafeSelectors = {}; +var DOMAIN_REGEX = /^[^/]+/; +function sbp(selector, ...data) { + const domain = domainFromSelector(selector); + if (!selectors[selector]) { + throw new Error(`SBP: selector not registered: ${selector}`); + } + for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) { + if (filters) { + for (const filter of filters) { + if (filter(domain, selector, data) === false) + return; + } + } + } + return selectors[selector].call(domains[domain].state, ...data); +} +function domainFromSelector(selector) { + const domainLookup = DOMAIN_REGEX.exec(selector); + if (domainLookup === null) { + throw new Error(`SBP: selector missing domain: ${selector}`); + } + return domainLookup[0]; +} +var SBP_BASE_SELECTORS = { + "sbp/selectors/register": function(sels) { + const registered = []; + for (const selector in sels) { + const domain = domainFromSelector(selector); + if (selectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`); + } else if (typeof sels[selector] === "function") { + if (unsafeSelectors[selector]) { + (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`); + } + const fn = selectors[selector] = sels[selector]; + registered.push(selector); + if (!domains[domain]) { + domains[domain] = { state: {} }; + } + if (selector === `${domain}/_init`) { + fn.call(domains[domain].state); + } + } + } + return registered; + }, + "sbp/selectors/unregister": function(sels) { + for (const selector of sels) { + if (!unsafeSelectors[selector]) { + throw new Error(`SBP: can't unregister locked selector: ${selector}`); + } + delete selectors[selector]; + } + }, + "sbp/selectors/overwrite": function(sels) { + sbp("sbp/selectors/unregister", Object.keys(sels)); + return sbp("sbp/selectors/register", sels); + }, + "sbp/selectors/unsafe": function(sels) { + for (const selector of sels) { + if (selectors[selector]) { + throw new Error("unsafe must be called before registering selector"); + } + unsafeSelectors[selector] = true; + } + }, + "sbp/selectors/lock": function(sels) { + for (const selector of sels) { + delete unsafeSelectors[selector]; + } + }, + "sbp/selectors/fn": function(sel) { + return selectors[sel]; + }, + "sbp/filters/global/add": function(filter) { + globalFilters.push(filter); + }, + "sbp/filters/domain/add": function(domain, filter) { + if (!domainFilters[domain]) + domainFilters[domain] = []; + domainFilters[domain].push(filter); + }, + "sbp/filters/selector/add": function(selector, filter) { + if (!selectorFilters[selector]) + selectorFilters[selector] = []; + selectorFilters[selector].push(filter); + } +}; +SBP_BASE_SELECTORS["sbp/selectors/register"](SBP_BASE_SELECTORS); +var module_default = sbp; + +// frontend/common/common.js +import { default as default2 } from "vue"; + +// frontend/common/vSafeHtml.js +import dompurify from "dompurify"; +import Vue from "vue"; + +// frontend/model/contracts/shared/giLodash.js +function cloneDeep(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +// frontend/common/vSafeHtml.js +var defaultConfig = { + ALLOWED_ATTR: ["class"], + ALLOWED_TAGS: ["b", "br", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: true +}; +var transform = (el, binding) => { + if (binding.oldValue !== binding.value) { + let config = defaultConfig; + if (binding.arg === "a") { + config = cloneDeep(config); + config.ALLOWED_ATTR.push("href", "target"); + config.ALLOWED_TAGS.push("a"); + } + el.textContent = ""; + el.appendChild(dompurify.sanitize(binding.value, config)); + } +}; +Vue.directive("safe-html", { + bind: transform, + update: transform +}); + +// frontend/common/translations.js +import dompurify2 from "dompurify"; +import Vue2 from "vue"; + +// frontend/common/stringTemplate.js +var nargs = /\{([0-9a-zA-Z_]+)\}/g; +function template(string, ...args) { + const firstArg = args[0]; + const replacementsByKey = typeof firstArg === "object" && firstArg !== null ? firstArg : args; + return string.replace(nargs, function replaceArg(match, capture, index) { + if (string[index - 1] === "{" && string[index + match.length] === "}") { + return capture; + } + const maybeReplacement = Object.prototype.hasOwnProperty.call(replacementsByKey, capture) ? replacementsByKey[capture] : void 0; + if (maybeReplacement === null || maybeReplacement === void 0) { + return ""; + } + return String(maybeReplacement); + }); +} + +// frontend/common/translations.js +Vue2.prototype.L = L; +Vue2.prototype.LTags = LTags; +var defaultLanguage = "en-US"; +var defaultLanguageCode = "en"; +var defaultTranslationTable = {}; +var dompurifyConfig = { + ...defaultConfig, + ALLOWED_ATTR: ["class", "href", "rel", "target"], + ALLOWED_TAGS: ["a", "b", "br", "button", "em", "i", "p", "small", "span", "strong", "sub", "sup", "u"], + RETURN_DOM_FRAGMENT: false +}; +var currentLanguage = defaultLanguage; +var currentLanguageCode = defaultLanguage.split("-")[0]; +var currentTranslationTable = defaultTranslationTable; +module_default("sbp/selectors/register", { + "translations/init": async function init(language) { + const [languageCode] = language.toLowerCase().split("-"); + if (language.toLowerCase() === currentLanguage.toLowerCase()) + return; + if (languageCode === currentLanguageCode) + return; + if (languageCode === defaultLanguageCode) { + currentLanguage = defaultLanguage; + currentLanguageCode = defaultLanguageCode; + currentTranslationTable = defaultTranslationTable; + return; + } + try { + currentTranslationTable = await module_default("backend/translations/get", language) || defaultTranslationTable; + currentLanguage = language; + currentLanguageCode = languageCode; + } catch (error) { + console.error(error); + } + } +}); +function LTags(...tags) { + const o = { + "br_": "
" + }; + for (const tag of tags) { + o[`${tag}_`] = `<${tag}>`; + o[`_${tag}`] = ``; + } + return o; +} +function L(key, args) { + return template(currentTranslationTable[key] || key, args).replace(/\s(?=[;:?!])/g, " "); +} +function sanitize(inputString) { + return dompurify2.sanitize(inputString, dompurifyConfig); +} +Vue2.component("i18n", { + functional: true, + props: { + args: [Object, Array], + tag: { + type: String, + default: "span" + }, + compile: Boolean + }, + render: function(h, context) { + const text = context.children[0].text; + const translation = L(text, context.props.args || {}); + if (!translation) { + console.warn("The following i18n text was not translated correctly:", text); + return h(context.props.tag, context.data, text); + } + if (context.props.tag === "a" && context.data.attrs.target === "_blank") { + context.data.attrs.rel = "noopener noreferrer"; + } + if (context.props.compile) { + const result = Vue2.compile("" + sanitize(translation) + ""); + return result.render.call({ + _c: (tag, ...args) => { + if (tag === "wrap") { + return h(context.props.tag, context.data, ...args); + } else { + return h(tag, ...args); + } + }, + _v: (x) => x + }); + } else { + if (!context.data.domProps) + context.data.domProps = {}; + context.data.domProps.innerHTML = sanitize(translation); + return h(context.props.tag, context.data); + } + } +}); + +// frontend/model/contracts/misc/flowTyper.js +var EMPTY_VALUE = Symbol("@@empty"); +var isEmpty = (v) => v === EMPTY_VALUE; +var isNil = (v) => v === null; +var isUndef = (v) => typeof v === "undefined"; +var isBoolean = (v) => typeof v === "boolean"; +var isNumber = (v) => typeof v === "number"; +var isObject = (v) => !isNil(v) && typeof v === "object"; +var isFunction = (v) => typeof v === "function"; +var getType = (typeFn, _options) => { + if (isFunction(typeFn.type)) + return typeFn.type(_options); + return typeFn.name || "?"; +}; +var TypeValidatorError = class extends Error { + expectedType; + valueType; + value; + typeScope; + sourceFile; + constructor(message, expectedType, valueType, value, typeName = "", typeScope = "") { + const errMessage = message || `invalid "${valueType}" value type; ${typeName || expectedType} type expected`; + super(errMessage); + this.expectedType = expectedType; + this.valueType = valueType; + this.value = value; + this.typeScope = typeScope || ""; + this.sourceFile = this.getSourceFile(); + this.message = `${errMessage} +${this.getErrorInfo()}`; + this.name = this.constructor.name; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, TypeValidatorError); + } + } + getSourceFile() { + const fileNames = this.stack.match(/(\/[\w_\-.]+)+(\.\w+:\d+:\d+)/g) || []; + return fileNames.find((fileName) => fileName.indexOf("/flowTyper-js/dist/") === -1) || ""; + } + getErrorInfo() { + return ` + file ${this.sourceFile} + scope ${this.typeScope} + expected ${this.expectedType.replace(/\n/g, "")} + type ${this.valueType} + value ${this.value} +`; + } +}; +var validatorError = (typeFn, value, scope, message, expectedType, valueType) => { + return new TypeValidatorError(message, expectedType || getType(typeFn), valueType || typeof value, JSON.stringify(value), typeFn.name, scope); +}; +var literalOf = (primitive) => { + function literal(value, _scope = "") { + if (isEmpty(value) || value === primitive) + return primitive; + throw validatorError(literal, value, _scope); + } + literal.type = () => { + if (isBoolean(primitive)) + return `${primitive ? "true" : "false"}`; + else + return `"${primitive}"`; + }; + return literal; +}; +var object = function(value) { + if (isEmpty(value)) + return {}; + if (isObject(value) && !Array.isArray(value)) { + return Object.assign({}, value); + } + throw validatorError(object, value); +}; +var objectOf = (typeObj, _scope = "Object") => { + function object2(value) { + const o = object(value); + const typeAttrs = Object.keys(typeObj); + const unknownAttr = Object.keys(o).find((attr) => !typeAttrs.includes(attr)); + if (unknownAttr) { + throw validatorError(object2, value, _scope, `missing object property '${unknownAttr}' in ${_scope} type`); + } + const undefAttr = typeAttrs.find((property) => { + const propertyTypeFn = typeObj[property]; + return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + }); + if (undefAttr) { + throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); + } + const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { + const typeFn = typeObj[key]; + if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + return Object.assign(acc, {}); + } else { + return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); + } + }; + return typeAttrs.reduce(reducer, {}); + } + object2.type = () => { + const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + return `{| + ${props.join(",\n ")} +|}`; + }; + return object2; +}; +function undef(value, _scope = "") { + if (isEmpty(value) || isUndef(value)) + return void 0; + throw validatorError(undef, value, _scope); +} +undef.type = () => "void"; +var number = function number2(value, _scope = "") { + if (isEmpty(value)) + return 0; + if (isNumber(value)) + return value; + throw validatorError(number2, value, _scope); +}; +function unionOf_(...typeFuncs) { + function union(value, _scope = "") { + for (const typeFn of typeFuncs) { + try { + return typeFn(value, _scope); + } catch (_) { + } + } + throw validatorError(union, value, _scope); + } + union.type = () => `(${typeFuncs.map((fn) => getType(fn)).join(" | ")})`; + return union; +} +var unionOf = unionOf_; + +// frontend/model/contracts/shared/time.js +var MINS_MILLIS = 6e4; +var HOURS_MILLIS = 60 * MINS_MILLIS; +var DAYS_MILLIS = 24 * HOURS_MILLIS; +var MONTHS_MILLIS = 30 * DAYS_MILLIS; + +// frontend/model/contracts/shared/constants.js +var PROFILE_STATUS = { + ACTIVE: "active", + PENDING: "pending", + REMOVED: "removed" +}; +var PROPOSAL_RESULT = "proposal-result"; +var PROPOSAL_INVITE_MEMBER = "invite-member"; +var PROPOSAL_REMOVE_MEMBER = "remove-member"; +var PROPOSAL_GROUP_SETTING_CHANGE = "group-setting-change"; +var PROPOSAL_PROPOSAL_SETTING_CHANGE = "proposal-setting-change"; +var PROPOSAL_GENERIC = "generic"; +var STATUS_PASSED = "passed"; +var STATUS_FAILED = "failed"; + +// frontend/model/contracts/shared/voting/rules.js +var VOTE_AGAINST = ":against"; +var VOTE_INDIFFERENT = ":indifferent"; +var VOTE_UNDECIDED = ":undecided"; +var VOTE_FOR = ":for"; +var RULE_PERCENTAGE = "percentage"; +var RULE_DISAGREEMENT = "disagreement"; +var RULE_MULTI_CHOICE = "multi-choice"; +var getPopulation = (state) => Object.keys(state.profiles).filter((p) => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length; +var rules = { + [RULE_PERCENTAGE]: function(state, proposalType2, votes) { + votes = Object.values(votes); + let population = getPopulation(state); + if (proposalType2 === PROPOSAL_REMOVE_MEMBER) + population -= 1; + const defaultThreshold = state.settings.proposals[proposalType2].ruleSettings[RULE_PERCENTAGE].threshold; + const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population); + const totalIndifferent = votes.filter((x) => x === VOTE_INDIFFERENT).length; + const totalFor = votes.filter((x) => x === VOTE_FOR).length; + const totalAgainst = votes.filter((x) => x === VOTE_AGAINST).length; + const totalForOrAgainst = totalFor + totalAgainst; + const turnout = totalForOrAgainst + totalIndifferent; + const absent = population - turnout; + const neededToPass = Math.ceil(threshold * (population - totalIndifferent)); + console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType2}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population }); + if (totalFor >= neededToPass) { + return VOTE_FOR; + } + return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED; + }, + [RULE_DISAGREEMENT]: function(state, proposalType2, votes) { + votes = Object.values(votes); + const population = getPopulation(state); + const minimumMax = proposalType2 === PROPOSAL_REMOVE_MEMBER ? 2 : 1; + const thresholdOriginal = Math.max(state.settings.proposals[proposalType2].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax); + const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population); + const totalFor = votes.filter((x) => x === VOTE_FOR).length; + const totalAgainst = votes.filter((x) => x === VOTE_AGAINST).length; + const turnout = votes.length; + const absent = population - turnout; + console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType2}:`, { totalFor, totalAgainst, threshold, turnout, population, absent }); + if (totalAgainst >= threshold) { + return VOTE_AGAINST; + } + return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED; + }, + [RULE_MULTI_CHOICE]: function(state, proposalType2, votes) { + throw new Error("unimplemented!"); + } +}; +var rules_default = rules; +var ruleType = unionOf(...Object.keys(rules).map((k) => literalOf(k))); +var getThresholdAdjusted = (rule, threshold, groupSize) => { + const groupSizeVoting = Math.max(3, groupSize); + return { + [RULE_DISAGREEMENT]: () => { + return Math.min(groupSizeVoting - 1, threshold); + }, + [RULE_PERCENTAGE]: () => { + const minThreshold = 2 / groupSizeVoting; + return Math.max(minThreshold, threshold); + } + }[rule](); +}; + +// frontend/model/contracts/shared/voting/proposals.js +function archiveProposal({ state, proposalHash, proposal, contractID }) { + default2.delete(state.proposals, proposalHash); + module_default("gi.contracts/group/pushSideEffect", contractID, ["gi.contracts/group/archiveProposal", contractID, proposalHash, proposal]); +} +function buildInvitationUrl(groupId, inviteSecret) { + return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`; +} +var proposalSettingsType = objectOf({ + rule: ruleType, + expires_ms: number, + ruleSettings: objectOf({ + [RULE_PERCENTAGE]: objectOf({ threshold: number }), + [RULE_DISAGREEMENT]: objectOf({ threshold: number }) + }) +}); +function oneVoteToPass(proposalHash) { + const rootState = module_default("state/vuex/state"); + const state = rootState[rootState.currentGroupId]; + const proposal = state.proposals[proposalHash]; + const votes = Object.assign({}, proposal.votes); + const currentResult = rules_default[proposal.data.votingRule](state, proposal.data.proposalType, votes); + votes[String(Math.random())] = VOTE_FOR; + const newResult = rules_default[proposal.data.votingRule](state, proposal.data.proposalType, votes); + console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`); + return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR; +} +function voteAgainst(state, { meta, data, contractID }) { + const { proposalHash } = data; + const proposal = state.proposals[proposalHash]; + proposal.status = STATUS_FAILED; + module_default("okTurtles.events/emit", PROPOSAL_RESULT, state, VOTE_AGAINST, data); + archiveProposal({ state, proposalHash, proposal, contractID }); +} +var proposalDefaults = { + rule: RULE_PERCENTAGE, + expires_ms: 14 * DAYS_MILLIS, + ruleSettings: { + [RULE_PERCENTAGE]: { threshold: 0.66 }, + [RULE_DISAGREEMENT]: { threshold: 1 } + } +}; +var proposals = { + [PROPOSAL_INVITE_MEMBER]: { + defaults: proposalDefaults, + [VOTE_FOR]: function(state, { meta, data, contractID }) { + const { proposalHash } = data; + const proposal = state.proposals[proposalHash]; + proposal.payload = data.passPayload; + proposal.status = STATUS_PASSED; + const message = { meta, data: data.passPayload, contractID }; + module_default("gi.contracts/group/invite/process", message, state); + module_default("okTurtles.events/emit", PROPOSAL_RESULT, state, VOTE_FOR, data); + archiveProposal({ state, proposalHash, proposal, contractID }); + }, + [VOTE_AGAINST]: voteAgainst + }, + [PROPOSAL_REMOVE_MEMBER]: { + defaults: proposalDefaults, + [VOTE_FOR]: function(state, { meta, data, contractID }) { + const { proposalHash, passPayload } = data; + const proposal = state.proposals[proposalHash]; + proposal.status = STATUS_PASSED; + proposal.payload = passPayload; + const messageData = { + ...proposal.data.proposalData, + proposalHash, + proposalPayload: passPayload + }; + const message = { data: messageData, meta, contractID }; + module_default("gi.contracts/group/removeMember/process", message, state); + module_default("gi.contracts/group/pushSideEffect", contractID, ["gi.contracts/group/removeMember/sideEffect", message]); + archiveProposal({ state, proposalHash, proposal, contractID }); + }, + [VOTE_AGAINST]: voteAgainst + }, + [PROPOSAL_GROUP_SETTING_CHANGE]: { + defaults: proposalDefaults, + [VOTE_FOR]: function(state, { meta, data, contractID }) { + const { proposalHash } = data; + const proposal = state.proposals[proposalHash]; + proposal.status = STATUS_PASSED; + const { setting, proposedValue } = proposal.data.proposalData; + const message = { + meta, + data: { [setting]: proposedValue }, + contractID + }; + module_default("gi.contracts/group/updateSettings/process", message, state); + archiveProposal({ state, proposalHash, proposal, contractID }); + }, + [VOTE_AGAINST]: voteAgainst + }, + [PROPOSAL_PROPOSAL_SETTING_CHANGE]: { + defaults: proposalDefaults, + [VOTE_FOR]: function(state, { meta, data, contractID }) { + const { proposalHash } = data; + const proposal = state.proposals[proposalHash]; + proposal.status = STATUS_PASSED; + const message = { + meta, + data: proposal.data.proposalData, + contractID + }; + module_default("gi.contracts/group/updateAllVotingRules/process", message, state); + archiveProposal({ state, proposalHash, proposal, contractID }); + }, + [VOTE_AGAINST]: voteAgainst + }, + [PROPOSAL_GENERIC]: { + defaults: proposalDefaults, + [VOTE_FOR]: function(state, { meta, data, contractID }) { + const { proposalHash } = data; + const proposal = state.proposals[proposalHash]; + proposal.status = STATUS_PASSED; + module_default("okTurtles.events/emit", PROPOSAL_RESULT, state, VOTE_FOR, data); + archiveProposal({ state, proposalHash, proposal, contractID }); + }, + [VOTE_AGAINST]: voteAgainst + } +}; +var proposals_default = proposals; +var proposalType = unionOf(...Object.keys(proposals).map((k) => literalOf(k))); +export { + archiveProposal, + buildInvitationUrl, + proposals_default as default, + oneVoteToPass, + proposalDefaults, + proposalSettingsType, + proposalType +}; +//# sourceMappingURL=proposals.js.map diff --git a/test/contracts/shared/voting/proposals.js.map b/test/contracts/shared/voting/proposals.js.map new file mode 100644 index 0000000000..a595b17598 --- /dev/null +++ b/test/contracts/shared/voting/proposals.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../../../node_modules/@sbp/sbp/dist/module.mjs", "../../../../frontend/common/common.js", "../../../../frontend/common/vSafeHtml.js", "../../../../frontend/model/contracts/shared/giLodash.js", "../../../../frontend/common/translations.js", "../../../../frontend/common/stringTemplate.js", "../../../../frontend/model/contracts/misc/flowTyper.js", "../../../../frontend/model/contracts/shared/time.js", "../../../../frontend/model/contracts/shared/constants.js", "../../../../frontend/model/contracts/shared/voting/rules.js", "../../../../frontend/model/contracts/shared/voting/proposals.js"], + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n"], + "mappings": "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", + "names": [] +} From 23272a28948d1c1b3065cc795be55aa0cd0b7740 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 11 Sep 2022 22:47:15 +0200 Subject: [PATCH 19/43] Fix test:unit task --- Gruntfile.js | 2 +- backend/database.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 4d024d19d3..c0b0ea48a0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -684,7 +684,7 @@ module.exports = (grunt) => { killKeepAlive = this.async() }) - grunt.registerTask('test', ['build', 'exec:chelDeployAll', 'deno:start', 'exec:test', 'cypress', 'deno:stop', 'flow:stop']) + grunt.registerTask('test', ['build', 'exec:chelDeployAll', 'deno:start', 'exec:test', 'exec:testWithDeno', 'cypress', 'deno:stop', 'flow:stop']) grunt.registerTask('test:unit', ['deno:start', 'exec:test', 'exec:testWithDeno', 'deno:stop']) // ------------------------------------------------------------------------- diff --git a/backend/database.ts b/backend/database.ts index 571c92127c..a5be8c121f 100644 --- a/backend/database.ts +++ b/backend/database.ts @@ -18,7 +18,7 @@ const readTextFileAsync = Deno.readTextFile const writeFileAsync = Deno.writeFile const writeTextFileAsync = Deno.writeTextFile -const dirExists = async (pathname: numer) => { +const dirExists = async (pathname) => { try { const stats = await Deno.stat(pathname) return stats ?? stats.isDirectory() From b20992c8b7063b5401dbe9792839690a38e1250d Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 11 Sep 2022 22:47:46 +0200 Subject: [PATCH 20/43] Exit early when running in CI --- Gruntfile.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Gruntfile.js b/Gruntfile.js index c0b0ea48a0..4860da3e06 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,5 +1,7 @@ 'use strict' +if (process.env['CI']) process.exit(1) + // ======================= // Entry point. // From 80573a78cc0d265655668c7842af88fbd4b07419 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:07:27 +0200 Subject: [PATCH 21/43] Make backend and shared pass deno check --- backend/.eslintrc.json | 20 + backend/auth.ts | 6 +- backend/database.ts | 78 +-- backend/index.ts | 62 +- backend/pubsub.ts | 423 +++++++----- backend/router.ts | 363 ----------- backend/routes.ts | 63 +- backend/server.ts | 35 +- backend/types.ts | 24 +- frontend/.eslintrc.json | 32 + package-lock.json | 933 +++++++++++++++++++++++++-- package.json | 3 + scripts/process-shim.ts | 10 +- shared/domains/chelonia/GIMessage.ts | 47 +- shared/domains/chelonia/chelonia.ts | 147 ++++- shared/domains/chelonia/db.ts | 30 +- shared/domains/chelonia/errors.ts | 8 +- shared/domains/chelonia/internals.ts | 79 ++- shared/functions.ts | 22 +- shared/pubsub.ts | 525 +++++++-------- shared/types.ts | 10 +- 21 files changed, 1803 insertions(+), 1117 deletions(-) create mode 100644 backend/.eslintrc.json delete mode 100644 backend/router.ts create mode 100644 frontend/.eslintrc.json diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json new file mode 100644 index 0000000000..73ae078d61 --- /dev/null +++ b/backend/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "parserOptions": { + "parser": "@typescript-eslint/parser" + }, + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "plugins": [ + "@typescript-eslint", + "import" + ], + "rules": { + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "require-await": "error", + "quote-props": "off", + "dot-notation": "off", + "import/extensions": [ + 2, + "ignorePackages" + ] + } +} diff --git a/backend/auth.ts b/backend/auth.ts index 1576fcc9c6..2fff7f6ca5 100644 --- a/backend/auth.ts +++ b/backend/auth.ts @@ -5,7 +5,7 @@ import { verify, b64ToStr } from '~/shared/functions.ts' -const { BadRequest, PermissionDenied } = Deno.errors +const { PermissionDenied } = Deno.errors export default { name: 'gi-auth', @@ -20,12 +20,12 @@ export default { // NOTE: if you want to add any signature verification, do it here // eslint-disable-next-line no-constant-condition if (false) { - if (!scheme.includes('gi')) h.unauthenticated(new BadRequest('Bad authentication')) + if (!scheme.includes('gi')) h.unauthenticated(new PermissionDenied('Bad authentication')) try { json = JSON.parse(b64ToStr(json)) } catch (e) { - return h.unauthenticated(new BadRequest('Invalid token format')) + return h.unauthenticated(new PermissionDenied('Invalid token format')) } // http://hapijs.com/api/#serverauthschemename-scheme const isValid = verify(json.msg, json.key, json.sig) diff --git a/backend/database.ts b/backend/database.ts index a5be8c121f..c2342f68dc 100644 --- a/backend/database.ts +++ b/backend/database.ts @@ -1,36 +1,31 @@ +/* globals Deno */ import * as pathlib from 'path' -import sbp from "@sbp/sbp" -import { notFound } from 'pogo/lib/bang.ts' +import sbp from '@sbp/sbp' import '~/shared/domains/chelonia/db.ts' import { strToB64 } from '~/shared/functions.ts' -const CI = Deno.env.get('CI') -const GI_VERSION = Deno.env.get('GI_VERSION') const NODE_ENV = Deno.env.get('NODE_ENV') -const { AlreadyExists, BadRequest, NotFound } = Deno.errors +// Don't use errors from any server framework in this file. +const { AlreadyExists, NotFound, PermissionDenied } = Deno.errors const dataFolder = pathlib.resolve('./data') const production = NODE_ENV === 'production' const readFileAsync = Deno.readFile -const readTextFileAsync = Deno.readTextFile const writeFileAsync = Deno.writeFile -const writeTextFileAsync = Deno.writeTextFile -const dirExists = async (pathname) => { +const dirExists = async (pathname: string): Promise => { try { - const stats = await Deno.stat(pathname) - return stats ?? stats.isDirectory() + return (await Deno.stat(pathname)).isDirectory } catch { return false } } -const fileExists = async (pathname) => { +const fileExists = async (pathname: string): Promise => { try { - const stats = await Deno.stat(pathname) - return stats ?? stats.isFile() + return (await Deno.stat(pathname)).isFile } catch { return false } @@ -40,11 +35,12 @@ if (!(await dirExists(dataFolder))) { await Deno.mkdir(dataFolder, { mode: 0o750, recursive: true }) } -export default (sbp('sbp/selectors/register', { - 'backend/db/streamEntriesSince': async function (contractID: string, hash: string) { +// These selectors must not return bang errors. +export default sbp('sbp/selectors/register', { + 'backend/db/streamEntriesSince': async function (contractID: string, hash: string): Promise { let currentHEAD = await sbp('chelonia/db/latestHash', contractID) if (!currentHEAD) { - throw notFound(`contractID ${contractID} doesn't exist!`) + throw new NotFound(`contractID ${contractID} doesn't exist!`) } const chunks = ['['] try { @@ -69,13 +65,14 @@ export default (sbp('sbp/selectors/register', { } return chunks.join('') }, - 'backend/db/streamEntriesBefore': async function (before: string, limit: number): Promise { + 'backend/db/streamEntriesBefore': async function (before: string, limit: number): Promise { let currentHEAD = before let entry = await sbp('chelonia/db/getEntry', currentHEAD) if (!entry) { - throw notFound(`entry ${currentHEAD} doesn't exist!`) + throw new NotFound(`entry ${currentHEAD} doesn't exist!`) } - limit++ // to return `before` apart from the `limit` number of events + // To return `before` apart from the `limit` number of events. + limit++ const chunks = ['['] try { while (true) { @@ -100,12 +97,12 @@ export default (sbp('sbp/selectors/register', { } return chunks.join('') }, - 'backend/db/streamEntriesBetween': async function (startHash: string, endHash: string, offset: number): Promise { + 'backend/db/streamEntriesBetween': async function (startHash: string, endHash: string, offset: number): Promise { let isMet = false let currentHEAD = endHash let entry = await sbp('chelonia/db/getEntry', currentHEAD) if (!entry) { - throw notFound(`entry ${currentHEAD} doesn't exist!`) + throw new NotFound(`entry ${currentHEAD} doesn't exist!`) } const chunks = ['['] try { @@ -124,7 +121,6 @@ export default (sbp('sbp/selectors/register', { } else if (isMet) { offset-- } - currentHEAD = entry.message().previousHEAD if (!currentHEAD || (isMet && !offset)) { @@ -138,26 +134,31 @@ export default (sbp('sbp/selectors/register', { return chunks.join('') }, // ======================= - // wrapper methods to add / lookup names + // Wrapper methods to add / lookup names // ======================= - 'backend/db/registerName': async function (name: string, value: string) { - const lookup = await sbp('backend/db/lookupName', name) || null - if (lookup) { - if (!(lookup instanceof Error)) { - return new AlreadyExists(`in backend/db/registerName: ${name}`) - } - if (!(lookup instanceof NotFound)) { - // Throw if this is an error other than "not found". - throw lookup + 'backend/db/registerName': async function (name: string, value: string): Promise<{ name: string, value: string } | Error> { + try { + await sbp('backend/db/lookupName', name) + // If no error was thrown, then the given name has already been registered. + return new AlreadyExists(`in backend/db/registerName: ${name}`) + } catch (err) { + // Proceed ahead if this is a NotFound error. + if (err instanceof NotFound) { + await sbp('chelonia/db/set', namespaceKey(name), value) + return { name, value } } - // Otherwise it is a Boom.notFound(), proceed ahead. + // Otherwise it is an unexpected error, so rethrow it. + throw err } - await sbp('chelonia/db/set', namespaceKey(name), value) - return { name, value } }, 'backend/db/lookupName': async function (name: string) { const value = await sbp('chelonia/db/get', namespaceKey(name)) - return value || new NotFound(name) + console.log('value:', value) + if (value !== undefined) { + return value + } else { + throw new NotFound(name) + } }, // ======================= // Filesystem API @@ -186,16 +187,17 @@ export default (sbp('sbp/selectors/register', { } return await writeFileAsync(filepath, data) } -})) +}) function namespaceKey (name: string): string { return 'name=' + name } +// TODO: maybe Deno's own filesystem permissions can make this unnecessary. function throwIfFileOutsideDataDir (filename: string): string { const filepath = pathlib.resolve(pathlib.join(dataFolder, filename)) if (!filepath.startsWith(dataFolder)) { - throw new BadRequest(`bad name: ${filename}`) + throw new PermissionDenied(`bad name: ${filename}`) } return filepath } diff --git a/backend/index.ts b/backend/index.ts index 9dc8eac610..9b811baa03 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -1,33 +1,32 @@ -import { bold } from "fmt/colors.ts" +declare var process: any -import sbp from "@sbp/sbp" -import "@sbp/okturtles.data" -import "@sbp/okturtles.events" +import { bold } from 'fmt/colors.ts' +import sbp from '@sbp/sbp' +import '@sbp/okturtles.data' +import '@sbp/okturtles.events' +import { notFound } from 'pogo/lib/bang.ts' + +import '~/scripts/process-shim.ts' import { SERVER_RUNNING } from './events.ts' import { PUBSUB_INSTANCE } from './instance-keys.ts' +import type { PubsubClient, PubsubServer } from './pubsub.ts' -window.logger = function (err) { +// @ts-ignore +globalThis.logger = function (err: Error) { console.error(err) err.stack && console.error(err.stack) - return err // routes.ts is written in a way that depends on this returning the error -} - -const process = window.process = { - env: { - get (key) { - return Deno.env.get(key) - }, - set (key, value) { - return Deno.env.set(key, value) - } + if (err instanceof Deno.errors.NotFound) { + console.log('Returning notFound()', err.message) + return notFound(err.message) } + return err // routes.ts is written in a way that depends on this returning the error } -const dontLog = { 'backend/server/broadcastEntry': true } +const dontLog: Record = { 'backend/server/broadcastEntry': true } -function logSBP (domain, selector, data) { - if (!dontLog[selector]) { +function logSBP (domain: string, selector: string, data: any) { + if (!(selector in dontLog)) { console.log(bold(`[sbp] ${selector}`), data) } } @@ -39,7 +38,7 @@ export default (new Promise((resolve, reject) => { try { sbp('okTurtles.events/on', SERVER_RUNNING, function () { console.log(bold('backend startup sequence complete.')) - resolve() + resolve(undefined) }) // Call this after we've registered listener for `SERVER_RUNNING`. import('./server.ts') @@ -48,15 +47,14 @@ export default (new Promise((resolve, reject) => { } })) -const shutdownFn = function (message) { - sbp('okTurtles.data/apply', PUBSUB_INSTANCE, function (pubsub) { - console.log('message received in child, shutting down...', message) +const shutdownFn = function () { + sbp('okTurtles.data/apply', PUBSUB_INSTANCE, function (pubsub: PubsubServer) { + console.log('message received in child, shutting down...') pubsub.on('close', async function () { try { await sbp('backend/server/stop') console.log('Backend server down') - process.send({}) // tell grunt we've successfully shutdown the server - process.nextTick(() => Deno.exit(0)) // triple-check we quit :P + Deno.exit(0) } catch (err) { console.error('Error during shutdown:', err) Deno.exit(1) @@ -65,24 +63,14 @@ const shutdownFn = function (message) { pubsub.close() // Since `ws` v8.0, `WebSocketServer.close()` no longer closes remaining connections. // See https://github.com/websockets/ws/commit/df7de574a07115e2321fdb5fc9b2d0fea55d27e8 - pubsub.clients.forEach(client => client.terminate()) + pubsub.clients.forEach((client: PubsubClient) => client.terminate()) }) } -// Sent by Nodemon. -addEventListener('SIGUSR2', shutdownFn) - -// When spawned by another process, -// listen for message events to cleanly shutdown and relinquish port. -addEventListener('message', shutdownFn) +Deno.addSignalListener('SIGUSR2', shutdownFn) // Equivalent to the `uncaughtException` event in Nodejs. addEventListener('error', (event) => { console.error('[server] Unhandled exception:', event) Deno.exit(1) }) - -addEventListener('unhandledRejection', (reason, p) => { - console.error('[server] Unhandled promise rejection:', p, 'reason:', reason) - Deno.exit(1) -}) diff --git a/backend/pubsub.ts b/backend/pubsub.ts index d9cfc2c3cc..8a207cd21a 100644 --- a/backend/pubsub.ts +++ b/backend/pubsub.ts @@ -1,8 +1,28 @@ +type JSONType = ReturnType + +type Message = { + [key: string]: JSONType, + type: string +} + +type SubMessage = { + contractID: string + dontBroadcast?: boolean +} + +type UnsubMessage = { + contractID: string + dontBroadcast?: boolean +} + +type PubsubClientEventName = 'close' | 'message' +type PubsubServerEventName = 'close' | 'connection' | 'error' | 'headers' | 'listening' + import { acceptWebSocket, isWebSocketCloseEvent, isWebSocketPingEvent, -} from "https://deno.land/std@0.92.0/ws/mod.ts"; +} from 'https://deno.land/std@0.92.0/ws/mod.ts' import { messageParser } from '~/shared/pubsub.ts' @@ -20,12 +40,14 @@ const tag = '[pubsub]' const generateSocketID = (() => { let counter = 0 - return (debugID: string) => String(counter++) + (debugID ? '-' + debugID : '') + return (debugID: string): string => String(counter++) + (debugID ? '-' + debugID : '') })() -const log: Function = console.log.bind(console, tag) -log.debug = console.debug.bind(console, tag) -log.error = console.error.bind(console, tag) +const logger = { + log: console.log.bind(console, tag), + debug: console.debug.bind(console, tag), + error: console.error.bind(console, tag) +} // ====== API ====== // @@ -45,64 +67,164 @@ export function createResponse (type: string, data: Object): string { return JSON.stringify({ type, data }) } -export function createServer(options?: Object = {}) { - const server = { - clients: new Set(), - customServerEventHandlers: Object.create(null), - customSocketEventHandlers: Object.create(null), - handleUpgradeableRequest, - messageHandlers: { ...defaultMessageHandlers, ...options.customMessageHandlers }, - options: { ...defaultOptions, ...options }, - get port () { return rawHttpServer.listener?.addr.port }, - queuesByEventName: new Map(), - subscribersByContractID: Object.create(null), - }; - - function handleUpgradeableRequest(request: Request): Response { +export class PubsubClient { + id: string + activeSinceLastPing: boolean + pinged: boolean + server: PubsubServer + socket: WebSocket + subscriptions: Set + + constructor (socket: WebSocket, server: PubsubServer, debugID = '') { + this.id = generateSocketID(debugID) + this.activeSinceLastPing = true + this.pinged = false + this.server = server + this.socket = socket + this.subscriptions = new Set() + } + + get readyState () { + return this.socket.readyState + } + + rejectMessageAndTerminate (request: Message) { + // TODO: wait for response to be sent before terminating. + this.socket.send(createErrorResponse({ ...request })) + this.terminate() + } + + send (data: string | ArrayBufferLike | Blob | ArrayBufferView): void { + const { socket } = this + if (socket.readyState === WebSocket.OPEN) { + this.socket.send(data) + } else { + // TODO: enqueue pending data. + } + } + + terminate () { + const { server, socket } = this + internalClientEventHandlers.close.call(this, 1000, '') + // Remove listeners for socket events, i.e. events emitted on a socket object. + ;['close', 'error', 'message', 'ping', 'pong'].forEach((eventName: string) => { + socket.removeEventListener(eventName, internalClientEventHandlers[eventName as PubsubClientEventName] as EventListener) + if (typeof server.customClientEventHandlers[eventName] === 'function') { + socket.removeEventListener(eventName as keyof WebSocketEventMap, server.customClientEventHandlers[eventName] as EventListener) + } + }) + socket.close() + } +} + +export class PubsubServer { + clients: Set + customServerEventHandlers: Record + customClientEventHandlers: Record + messageHandlers: Record + options: any + pingIntervalID?: number + queuesByEventName: Map> + subscribersByContractID: Record> + + constructor (options: Object = {}) { + this.clients = new Set() + this.customServerEventHandlers = Object.create(null) + this.customClientEventHandlers = Object.create(null) + this.messageHandlers = { ...defaultMessageHandlers, ...(options as any).customMessageHandlers } + this.options = { ...defaultOptions, ...options } + this.queuesByEventName = new Map() + this.subscribersByContractID = Object.create(null) + } + + close () { + this.clients.forEach(client => client.terminate()) + } + + handleUpgradeableRequest (request: Request): Response { + const server = this const { socket, response } = Deno.upgradeWebSocket(request) // Our code socket.onopen = () => { - server.clients.add(socket); server.emit('connection', socket, request) - }; - return response; + } + return response } - server.emit = (name, ...args) => { - const queue = server.queuesByEventName.get(name) ?? emptySet; + emit (name: string, ...args: any[]) { + const server = this + const queue = server.queuesByEventName.get(name) ?? emptySet try { - for(const callback of queue) { - Function.prototype.call.call(callback, server, ...args) + for (const callback of queue) { + Function.prototype.call.call(callback as Function, server, ...args) } } catch (error) { - if(server.queuesByEventName.has('error')) { - server.emit('error', error); + if (server.queuesByEventName.has('error')) { + server.emit('error', error) } else { - throw error; + throw error } } - }; + } - server.off = (name, callback) => { - const queue = server.queuesByEventName.get(name) ?? emptySet; - queue.delete(callback); - }; + off (name: string, callback: Function) { + const server = this + const queue = server.queuesByEventName.get(name) ?? emptySet + queue.delete(callback) + } - server.on = (name, callback) => { - if(!server.queuesByEventName.has(name)) { - server.queuesByEventName.set(name, new Set()); + on (name: string, callback: Function) { + const server = this + if (!server.queuesByEventName.has(name)) { + server.queuesByEventName.set(name, new Set()) } - const queue = server.queuesByEventName.get(name); - queue.add(callback); - }; + const queue = server.queuesByEventName.get(name) + queue?.add(callback) + } + + get port () { + return this.options.rawHttpServer?.listener?.addr?.port + } + + /** + * Broadcasts a message, ignoring clients which are not open. + * + * @param message + * @param to - The intended recipients of the message. Defaults to every open client socket. + * @param except - A recipient to exclude. Optional. + */ + broadcast ( + message: string, + { to, except }: { to?: Iterable, except?: PubsubClient } + ) { + const server = this + for (const client of to || server.clients) { + if (client.readyState === WebSocket.OPEN && client !== except) { + client.send(message) + } + } + } + + // Enumerates the subscribers of a given contract. + * enumerateSubscribers (contractID: string): Iterable { + const server = this + if (contractID in server.subscribersByContractID) { + yield * server.subscribersByContractID[contractID] + } + } +} + +export function createServer(options = {}) { + const server = new PubsubServer(options) // Add listeners for server events, i.e. events emitted on the server object. - Object.keys(internalServerHandlers).forEach((name) => { - server.on(name, (...args) => { + Object.keys(internalServerHandlers).forEach((name: string) => { + server.on(name, (...args: any[]) => { try { // Always call the default handler first. - internalServerHandlers[name]?.call(server, ...args) - server.customServerEventHandlers[name]?.call(server, ...args) + // @ts-ignore TS2556 [ERROR]: A spread argument must either have a tuple type or be passed to a rest parameter. + internalServerHandlers[name as PubsubServerEventName]?.call(server, ...args) + server.customServerEventHandlers[name as PubsubServerEventName]?.call(server, ...args) } catch (error) { server.emit('error', error) } @@ -112,23 +234,23 @@ export function createServer(options?: Object = {}) { if (server.options.pingInterval > 0) { server.pingIntervalID = setInterval(() => { if (server.clients.size && server.options.logPingRounds) { - log.debug('Pinging clients') + logger.debug('Pinging clients') } - server.clients.forEach((client) => { + server.clients.forEach((client: PubsubClient) => { if (client.pinged && !client.activeSinceLastPing) { - log(`Disconnecting irresponsive client ${client.id}`) + logger.log(`Disconnecting irresponsive client ${client.id}`) return client.terminate() } if (client.readyState === WebSocket.OPEN) { - client.send(createMessage(PING, Date.now()), () => { - client.activeSinceLastPing = false - client.pinged = true - }) + client.send(createMessage(PING, Date.now())) + // TODO: wait for the message to be sent. + client.activeSinceLastPing = false + client.pinged = true } }) }, server.options.pingInterval) } - return Object.assign(server, publicMethods) + return server } export function isUpgradeableRequest (request: Request): boolean { @@ -146,116 +268,115 @@ const defaultOptions = { // Internal default handlers for server events. // The `this` binding refers to the server object. -const internalServerHandlers = { +const internalServerHandlers = Object.freeze({ close () { - log('Server closed') + logger.log('Server closed') }, /** * Emitted when a connection handshake completes. * * @see https://github.com/websockets/ws/blob/master/doc/ws.md#event-connection - * @param {ws.WebSocket} socket - The client socket that connected. - * @param {http.IncomingMessage} request - The underlying Node http GET request. + * @param {WebSocket} socket - The client socket that connected. + * @param {Request} request - The incoming http GET request. */ - connection (socket: Object, request: Object) { - console.log('connection:', request.url) + connection (this: PubsubServer, socket: WebSocket, request: Request) { + logger.log('connection:', request.url) const server = this const url = request.url const urlSearch = url.includes('?') ? url.slice(url.lastIndexOf('?')) : '' const debugID = new URLSearchParams(urlSearch).get('debugID') || '' - socket.id = generateSocketID(debugID) - socket.activeSinceLastPing = true - socket.pinged = false - socket.server = server - socket.subscriptions = new Set() + + const client = new PubsubClient(socket, server) + client.id = generateSocketID(debugID) + client.activeSinceLastPing = true + server.clients.add(client) - log(`Socket ${socket.id} connected. Total: ${this.clients.size}`) + logger.log(`Client ${client.id} connected. Total: ${this.clients.size}`) // Add listeners for socket events, i.e. events emitted on a socket object. - if (!server.usingLegacyDenoWS) { - ['close', 'error', 'message', 'ping', 'pong'].forEach((eventName) => { - socket.addEventListener(eventName, (...args) => { - // Logging of 'message' events is handled in the default 'message' event handler. - if (eventName !== 'message') { - log(`Event '${eventName}' on socket ${socket.id}`, ...args.map(arg => String(arg))) - } - try { - (internalSocketEventHandlers)[eventName]?.call(socket, ...args) - server.customSocketEventHandlers[eventName]?.call(socket, ...args) - } catch (error) { - server.emit('error', error) - server.terminateSocket(socket) - } - }) + ;['close', 'error', 'message', 'ping', 'pong'].forEach((eventName: string) => { + socket.addEventListener(eventName, (...args) => { + // Logging of 'message' events is handled in the default 'message' event handler. + if (eventName !== 'message') { + logger.log(`Event '${eventName}' on client ${client.id}`, ...args.map(arg => String(arg))) + } + try { + // @ts-ignore TS2554 [ERROR]: Expected 3 arguments, but got 2. + (internalClientEventHandlers)[eventName as PubsubClientEventName]?.call(client, ...args) + server.customClientEventHandlers[eventName]?.call(client, ...args) + } catch (error) { + server.emit('error', error) + client.terminate() + } }) - } + }) }, error (error: Error) { - log.error('Server error:', error) + logger.error('Server error:', error) }, headers () { }, listening () { - log('Server listening') + logger.log('Server listening') } -} +}) // Default handlers for server-side client socket events. // The `this` binding refers to the connected `ws` socket object. -const internalSocketEventHandlers = { - close (code: string, reason: string) { - const socket = this - const { server, id: socketID } = this +const internalClientEventHandlers = Object.freeze({ + close (this: PubsubClient, code: number, reason: string) { + const client = this + const { server, socket, id: socketID } = this - // Notify other client sockets that this one has left any room they shared. - for (const contractID of socket.subscriptions) { + // Notify other clients that this one has left any room they shared. + for (const contractID of client.subscriptions) { const subscribers = server.subscribersByContractID[contractID] // Remove this socket from the subscribers of the given contract. - subscribers.delete(socket) + subscribers.delete(client) const notification = createNotification(UNSUB, { contractID, socketID }) server.broadcast(notification, { to: subscribers }) } - socket.subscriptions.clear() + client.subscriptions.clear() // Additional code. - socket.server.clients.delete(socket) + server.clients.delete(client) }, - message (event: MessageEvent) { - const socket = this - const { server } = this - const { type, data } = server.usingLegacyDenoWS ? { type: 'message', data: event } : event + message (this: PubsubClient, event: MessageEvent) { + const client = this + const { server, socket } = this + const { type, data } = event const text = data let msg: Message = { type: '' } try { msg = messageParser(data) } catch (error) { - log.error(`Malformed message: ${error.message}`) - server.rejectMessageAndTerminateSocket(msg, socket) + logger.error(`Malformed message: ${error.message}`) + client.rejectMessageAndTerminate(msg) return } // Now that we have successfully parsed the message, we can log it. if (msg.type !== 'pong' || server.options.logPongMessages) { - log(`Received '${msg.type}' on socket ${socket.id}`, text) + logger.log(`Received '${msg.type}' on client ${client.id}`, text) } // The socket can be marked as active since it just received a message. - socket.activeSinceLastPing = true + client.activeSinceLastPing = true const handler = server.messageHandlers[msg.type] if (handler) { try { - handler.call(socket, msg) + handler.call(client, msg) } catch (error) { // Log the error message and stack trace but do not send it to the client. - log.error(error) - server.rejectMessageAndTerminateSocket(msg, socket) + logger.error(error) + client.rejectMessageAndTerminate(msg) } } else { - log.error(`Unhandled message type: ${msg.type}`) - server.rejectMessageAndTerminateSocket(msg, socket) + logger.error(`Unhandled message type: ${msg.type}`) + client.rejectMessageAndTerminate(msg) } } -} +}) export const NOTIFICATION_TYPE = { APP_VERSION: 'app-version', @@ -269,110 +390,64 @@ const SUB = 'sub' const UNSUB = 'unsub' const SUCCESS = 'success' -// These handlers receive the connected `ws` socket through the `this` binding. +// These handlers receive the connected PubsubClient instance through the `this` binding. const defaultMessageHandlers = { - [PONG] (msg: Message) { - const socket = this + [PONG] (this: PubsubClient, msg: Message) { + const client = this // const timestamp = Number(msg.data) // const latency = Date.now() - timestamp - socket.activeSinceLastPing = true + client.activeSinceLastPing = true }, - [PUB] (msg: Message) { + [PUB] (this: PubsubClient, msg: Message) { // Currently unused. }, - [SUB] ({ contractID, dontBroadcast }: SubMessage) { - const socket = this - const { server, id: socketID } = this + [SUB] (this: PubsubClient, { contractID, dontBroadcast }: SubMessage) { + const client = this + const { server, socket, id: socketID } = this - if (!socket.subscriptions.has(contractID)) { + if (!client.subscriptions.has(contractID)) { // Add the given contract ID to our subscriptions. - socket.subscriptions.add(contractID) + client.subscriptions.add(contractID) if (!server.subscribersByContractID[contractID]) { server.subscribersByContractID[contractID] = new Set() } const subscribers = server.subscribersByContractID[contractID] - // Add this socket to the subscribers of the given contract. - subscribers.add(socket) + // Add this client to the subscribers of the given contract. + subscribers.add(client) if (!dontBroadcast) { // Broadcast a notification to every other open subscriber. const notification = createNotification(SUB, { contractID, socketID }) - server.broadcast(notification, { to: subscribers, except: socket }) + server.broadcast(notification, { to: subscribers, except: client }) } } else { - log('Already subscribed to', contractID) + logger.log('Already subscribed to', contractID) } socket.send(createResponse(SUCCESS, { type: SUB, contractID })) }, - [UNSUB] ({ contractID, dontBroadcast }: UnsubMessage) { - const socket = this - const { server, id: socketID } = this + [UNSUB] (this: PubsubClient, { contractID, dontBroadcast }: UnsubMessage) { + const client = this + const { server, socket, id: socketID } = this - if (socket.subscriptions.has(contractID)) { + if (client.subscriptions.has(contractID)) { // Remove the given contract ID from our subscriptions. - socket.subscriptions.delete(contractID) + client.subscriptions.delete(contractID) if (server.subscribersByContractID[contractID]) { const subscribers = server.subscribersByContractID[contractID] // Remove this socket from the subscribers of the given contract. - subscribers.delete(socket) + subscribers.delete(client) if (!dontBroadcast) { const notification = createNotification(UNSUB, { contractID, socketID }) // Broadcast a notification to every other open subscriber. - server.broadcast(notification, { to: subscribers, except: socket }) + server.broadcast(notification, { to: subscribers, except: client }) } } } else { - log('Was not subscribed to', contractID) + logger.log('Was not subscribed to', contractID) } socket.send(createResponse(SUCCESS, { type: UNSUB, contractID })) } } -const publicMethods = { - /** - * Broadcasts a message, ignoring clients which are not open. - * - * @param message - * @param to - The intended recipients of the message. Defaults to every open client socket. - * @param except - A recipient to exclude. Optional. - */ - broadcast ( - message: Message, - { to, except }: { to?: Iterable, except?: Object } - ) { - const server = this - - for (const client of to || server.clients) { - if (client.readyState === WebSocket.OPEN && client !== except) { - client.send(message) - } - } - }, - - // Enumerates the subscribers of a given contract. - * enumerateSubscribers (contractID: string): Iterable { - const server = this - - if (contractID in server.subscribersByContractID) { - yield * server.subscribersByContractID[contractID] - } - }, - - rejectMessageAndTerminateSocket (request: Message, socket: Object) { - socket.send(createErrorResponse({ ...request }), () => this.terminateSocket(socket)) - }, - - terminateSocket (socket: Object) { - const server = this - internalSocketEventHandlers.close.call(socket) - - // Remove listeners for socket events, i.e. events emitted on a socket object. - ;['close', 'error', 'message', 'ping', 'pong'].forEach((eventName) => { - socket.removeEventListener(eventName, (internalSocketEventHandlers)[eventName]) - socket.removeEventListener(eventName, server.customSocketEventHandlers[eventName]) - }) - socket.close() - }, -} diff --git a/backend/router.ts b/backend/router.ts deleted file mode 100644 index f454a54126..0000000000 --- a/backend/router.ts +++ /dev/null @@ -1,363 +0,0 @@ -import { - NormalizedRoute, - RequestParams, - Route, - RouteHandler, - RouteOptions, - MatchedRoute -} from './types.ts'; - -export type RouteOptionsHasHandler = RouteOptions & Required>; -export type RouteOptionsHasMethod = RouteOptions & Required>; -export type RouteOptionsHasPath = RouteOptions & Required>; -export type RouteOptionsHasHandlerAndMethod = RouteOptions & Required>; -export type RouteOptionsHasHandlerAndPath = RouteOptions & Required>; -export type RouteOptionsHasMethodAndPath = RouteOptions & Required>; -export type RequiredRouteOptions = RouteOptions & Required>; - -export type RoutesList = RequiredRouteOptions | Router | Iterable; -export type RoutesListHasHandler = RouteOptionsHasHandler | Router | Iterable; -export type RoutesListHasMethod = RouteOptionsHasMethod | Router | Iterable; -export type RoutesListHasPath = RouteOptionsHasPath | Router | string | Iterable; -export type RoutesListHasHandlerAndMethod = RouteOptionsHasHandlerAndMethod | Router | Iterable; -export type RoutesListHasHandlerAndPath = RouteOptionsHasHandlerAndPath | Router | Iterable; -export type RoutesListHasMethodAndPath = RouteOptionsHasMethodAndPath | Router | Iterable; - -const paramPattern = /\{(\w+)(?:\?|\*(?:[1-9]\d*)?)?\}/u; -const paramsPattern = new RegExp(paramPattern, paramPattern.flags + 'g'); - -const expandPath = (path: string): Array => { - return Array.from(path.matchAll(paramsPattern) as Iterable).flatMap((match) => { - const [param, name] = match; - const before = match.input.slice(0, match.index); - const after = match.input.slice(match.index + param.length); - - // Optional param, expand to paths WITH and WITHOUT the param - if (param.endsWith('?}')) { - const isWholeSegment = before.endsWith('/') && after.startsWith('/'); - const withParam = before + `{${name}}` + after; - const withoutParam = before + (isWholeSegment ? after.slice(1) : after) - return [withParam, withoutParam]; - } - - return []; - }); -}; - -const isDynamicSegment = (segment: string): boolean => { - return segment.startsWith('{') && segment.endsWith('}') && paramPattern.test(segment); -}; - -const getParamName = (segment: string): string => { - const param = segment.match(paramPattern); - return param ? param[1] : ''; -}; - -const toPathfinder = (segments: Array): string => { - const replacePart = (str: string) => { - return str && '.'; - }; - return segments.map(replacePart).join('/'); -}; - -/** - * Returns a human friendly text representation of the given route, such as GET /foo - */ -const toSignature = (route: NormalizedRoute): string => { - return route.method + ' ' + (route.vhost || '') + route.path; -}; - -const fingerprintPath = (path: string): string => { - return path.replace(paramsPattern, (param) => { - return param.endsWith('*}') ? '#' : '?'; - }); -}; - -const toConflictId = (route: NormalizedRoute): string => { - return toSignature({ - ...route, - path : fingerprintPath(route.path) - }); -} - -const sortRoutes = (left: NormalizedRoute, right: NormalizedRoute): number => { - const leftFirst = -1; - const rightFirst = 1; - const unchanged = 0; - - if (left.segments.filter(isDynamicSegment).length < - right.segments.filter(isDynamicSegment).length) { - return leftFirst; - } - if (left.segments.filter(isDynamicSegment).length > - right.segments.filter(isDynamicSegment).length) { - return rightFirst; - } - - if (left.segments.length < right.segments.length) { - return leftFirst; - } - if (left.segments.length > right.segments.length) { - return rightFirst; - } - - if (left.path < right.path) { - return leftFirst; - } - if (left.path > right.path) { - return rightFirst; - } - - return unchanged; -}; - -interface RoutingTable { - conflictIds: Map, - list: Array, - pathfinders: Map>, - wildcards: Array -} - -/** - * A router represents a collection of routes and determines which route will handle a given HTTP request. - * Use `pogo.router()` to create a router instance. - */ -export default class Router { - routes: RoutingTable; - constructor(route?: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler) { - this.routes = { - conflictIds : new Map(), - list : [], - pathfinders : new Map(), - wildcards : [] - }; - if (route) { - this.add(route, options, handler); - } - } - add(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; - add(route: RoutesListHasMethodAndPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; - add(route: RoutesListHasHandlerAndMethod, options: RouteOptionsHasPath, handler?: RouteHandler): this; - add(route: RoutesListHasHandlerAndPath, options: RouteOptionsHasMethod, handler?: RouteHandler): this; - add(route: RoutesListHasHandler, options: RouteOptionsHasMethodAndPath, handler?: RouteHandler): this; - add(route: RoutesListHasPath, options: RouteOptionsHasHandlerAndMethod, handler?: RouteHandler): this; - add(route: RoutesListHasPath, options: RouteOptionsHasMethod, handler: RouteHandler): this; - add(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; - add(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; - add(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { - if (route && typeof route === 'object' && Symbol.iterator in route) { - for (const settings of route as Iterable) { - this.add(settings, options, handler); - } - return this; - } - if (route instanceof Router) { - this.add(route.routes.list); - return this; - } - - const normalizedRoute = { - ...(typeof route === 'string' ? { path : route } : route), - ...(typeof options === 'function' ? { handler : options } : options), - ...(handler ? { handler } : null) - } as Route; - - if (typeof normalizedRoute.method === 'object' && Symbol.iterator in normalizedRoute.method) { - for (const method of normalizedRoute.method as Iterable) { - this.add({ - ...normalizedRoute, - method - }); - } - return this; - } - - if (typeof normalizedRoute.path === 'object' && Symbol.iterator in normalizedRoute.path) { - for (const path of normalizedRoute.path as Iterable) { - this.add({ - ...normalizedRoute, - path - }); - } - return this; - } - - const expandedPaths = expandPath(normalizedRoute.path); - if (expandedPaths.length > 0) { - this.add({ - ...normalizedRoute, - path : expandedPaths - }); - return this; - } - - const record: NormalizedRoute = { - ...normalizedRoute, - method : normalizedRoute.method.toUpperCase(), - paramNames : Array.from(normalizedRoute.path.matchAll(paramsPattern), (match) => { - return match[1]; - }), - segments : normalizedRoute.path.split('/'), - }; - - const conflictId = toConflictId(record); - const existingRoute = this.routes.conflictIds.get(conflictId); - if (existingRoute) { - const newRoute = toSignature(record); - const oldRoute = toSignature(existingRoute); - throw new Error(`Route conflict: new route "${newRoute}" conflicts with existing route "${oldRoute}"`); - } - - this.routes.conflictIds.set(conflictId, record); - - const hasWildcardParam = /\{\w+\*\}/u.test(record.path); - if (hasWildcardParam) { - this.routes.wildcards.push(record); - this.routes.wildcards.sort(sortRoutes); - } - else { - const pathfinder = toPathfinder(record.segments); - const pathfinderRoutes = this.routes.pathfinders.get(pathfinder) ?? []; - pathfinderRoutes.push(record); - pathfinderRoutes.sort(sortRoutes); - this.routes.pathfinders.set(pathfinder, pathfinderRoutes); - } - - this.routes.list.push(record); - this.routes.list.sort(sortRoutes); - - return this; - } - all(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; - all(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; - all(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; - all(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; - all(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; - all(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; - all(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { - const config = { - ...(typeof options === 'function' ? { handler : options } : options), - method : '*' - }; - this.add(route, config, handler); - return this; - } - delete(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; - delete(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; - delete(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; - delete(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; - delete(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; - delete(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; - delete(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { - const config = { - ...(typeof options === 'function' ? { handler : options } : options), - method : 'DELETE' - }; - this.add(route, config, handler); - return this; - } - get(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; - get(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; - get(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; - get(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; - get(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; - get(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; - get(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { - const config = { - ...(typeof options === 'function' ? { handler : options } : options), - method : 'GET' - }; - this.add(route, config, handler); - return this; - } - patch(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; - patch(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; - patch(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; - patch(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; - patch(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; - patch(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; - patch(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { - const config = { - ...(typeof options === 'function' ? { handler : options } : options), - method : 'PATCH' - }; - this.add(route, config, handler); - return this; - } - post(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; - post(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; - post(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; - post(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; - post(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; - post(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; - post(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { - const config = { - ...(typeof options === 'function' ? { handler : options } : options), - method : 'POST' - }; - this.add(route, config, handler); - return this; - } - put(route: RoutesListHasHandlerAndPath, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this; - put(route: RoutesListHasHandler, options: RouteOptionsHasPath, handler?: RouteHandler): this; - put(route: RoutesListHasPath, options: RouteOptionsHasHandler | RouteHandler, handler?: RouteHandler): this; - put(route: RoutesListHasPath, options: RouteOptions, handler: RouteHandler): this; - put(route: RoutesListHasMethod, options: RouteOptionsHasHandlerAndPath, handler?: RouteHandler): this; - put(route: RoutesListHasMethod, options: RouteOptionsHasPath, handler: RouteHandler): this; - put(route: RoutesList, options?: RouteOptions | RouteHandler, handler?: RouteHandler): this { - const config = { - ...(typeof options === 'function' ? { handler : options } : options), - method : 'PUT' - }; - this.add(route, config, handler); - return this; - } - lookup(method: string, path: string, host?: string): MatchedRoute | undefined { - const pathSegments = path.split('/'); - const pathfinder = toPathfinder(pathSegments); - - const matchRoute = (list: Array = []): NormalizedRoute | undefined => { - return list.find((route: NormalizedRoute) => { - const isMethodMatch = route.method === method || route.method === '*'; - if (!isMethodMatch) { - return false; - } - const isHostMatch = !host || !route.vhost || route.vhost === host; - if (!isHostMatch) { - return false; - } - const isStaticPath = route.paramNames.length === 0; - if (isStaticPath) { - return route.path === path; - } - - const matchesAllSegments = route.segments.every((routeSegment: string, index: number): boolean => { - return isDynamicSegment(routeSegment) || (routeSegment === pathSegments[index]); - }); - - const isPathMatch = matchesAllSegments && ((route.segments.length === pathSegments.length) || route.segments[route.segments.length - 1].endsWith('*}')); - - return isPathMatch; - }); - } - - const candidates = this.routes.pathfinders.get(pathfinder); - const wildcardRoutes = this.routes.wildcards; - const route = matchRoute(candidates) || matchRoute(wildcardRoutes); - - return route && { - ...route, - params : route.segments.reduce((params: RequestParams, routeSegment: string, index: number) => { - if (isDynamicSegment(routeSegment)) { - const name = getParamName(routeSegment); - params[name] = routeSegment.endsWith('*}') ? pathSegments.slice(index).join('/') : pathSegments[index]; - } - return params; - }, {}) - }; - } -} - -export { - toSignature -}; diff --git a/backend/routes.ts b/backend/routes.ts index 3c368f8707..2f7c0fedef 100644 --- a/backend/routes.ts +++ b/backend/routes.ts @@ -6,17 +6,22 @@ import sbp from '@sbp/sbp' import { GIMessage } from '~/shared/domains/chelonia/GIMessage.ts' import { blake32Hash } from '~/shared/functions.ts' -import { badRequest } from 'pogo/lib/bang.ts' +import { badRequest, notFound } from 'pogo/lib/bang.ts' import { Router } from 'pogo' +import type { RouteHandler } from 'pogo/lib/types.ts' +import type ServerRequest from 'pogo/lib/request.ts' +import Toolkit from 'pogo/lib/toolkit.ts' import './database.ts' import * as pathlib from 'path' +declare const logger: Function + export const router = new Router() -const route = new Proxy({}, { - get: function (obj, prop) { - return function (path: string, handler: Function | Object) { +const route: Record = new Proxy({}, { + get: function (obj: any, prop: string) { + return function (path: string, handler: RouteHandler) { router.add({ path, method: prop, handler }) } } @@ -27,7 +32,7 @@ const route = new Proxy({}, { // NOTE: We could get rid of this RESTful API and just rely on pubsub.js to do this // —BUT HTTP2 might be better than websockets and so we keep this around. // See related TODO in pubsub.js and the reddit discussion link. -route.POST('/event', async function (request, h) { +route.POST('/event', async function (request: ServerRequest, h: Toolkit) { try { console.log('/event handler') const payload = await request.raw.text() @@ -40,12 +45,11 @@ route.POST('/event', async function (request, h) { console.error(bold(yellow('ChelErrorDBBadPreviousHEAD')), err) return badRequest(err.message) } - console.error(err) - return err + return logger(err) } }) -route.GET('/eventsBefore/{before}/{limit}', async function (request, h) { +route.GET('/eventsBefore/{before}/{limit}', async function (request: ServerRequest, h: Toolkit) { try { const { before, limit } = request.params console.log('/eventsBefore:', before, limit) @@ -62,7 +66,7 @@ route.GET('/eventsBefore/{before}/{limit}', async function (request, h) { } }) -route.GET('/eventsBetween/{startHash}/{endHash}', async function (request, h) { +route.GET('/eventsBetween/{startHash}/{endHash}', async function (request: ServerRequest, h: Toolkit) { try { const { startHash, endHash } = request.params console.log('/eventsBetween:', startHash, endHash) @@ -81,7 +85,7 @@ route.GET('/eventsBetween/{startHash}/{endHash}', async function (request, h) { } }) -route.GET('/eventsSince/{contractID}/{since}', async function (request, h) { +route.GET('/eventsSince/{contractID}/{since}', async function (request: ServerRequest, h: Toolkit) { try { const { contractID, since } = request.params console.log('/eventsSince:', contractID, since) @@ -94,7 +98,7 @@ route.GET('/eventsSince/{contractID}/{since}', async function (request, h) { } }) -route.POST('/name', async function (request, h) { +route.POST('/name', async function (request: ServerRequest, h: Toolkit) { try { console.debug('/name', request.body) const payload = await request.raw.json() @@ -106,17 +110,16 @@ route.POST('/name', async function (request, h) { } }) -route.GET('/name/{name}', async function (request, h) { +route.GET('/name/{name}', async function (request: ServerRequest, h: Toolkit) { console.debug('GET /name/{name}', request.params.name) try { - const result = await sbp('backend/db/lookupName', request.params.name) - return result instanceof Deno.errors.NotFound ? request.response.code(404) : result + return await sbp('backend/db/lookupName', request.params.name) } catch (err) { - return (err) + return logger(err) } }) -route.GET('/latestHash/{contractID}', async function handler (request, h) { +route.GET('/latestHash/{contractID}', async function (request: ServerRequest, h: Toolkit) { try { const { contractID } = request.params const hash = await sbp('chelonia/db/latestHash', contractID) @@ -124,7 +127,7 @@ route.GET('/latestHash/{contractID}', async function handler (request, h) { request.response.header('cache-control', 'no-store') if (!hash) { console.warn(`[backend] latestHash not found for ${contractID}`) - return new Deno.errors.NotFound() + return notFound() } return hash } catch (err) { @@ -132,7 +135,7 @@ route.GET('/latestHash/{contractID}', async function handler (request, h) { } }) -route.GET('/time', function (request, h) { +route.GET('/time', function (request: ServerRequest, h: Toolkit) { request.response.header('cache-control', 'no-store') return new Date().toISOString() }) @@ -143,20 +146,20 @@ route.GET('/time', function (request, h) { // has a complete copy of the data and can act as a // new coordinating server... I don't like that. -route.POST('/file', async function (request, h) { +route.POST('/file', async function (request: ServerRequest, h: Toolkit) { try { console.log('FILE UPLOAD!') const formData = await request.raw.formData() - const data = formData.get('data') + const data = formData.get('data') as File const hash = formData.get('hash') if (!data) return badRequest('missing data') if (!hash) return badRequest('missing hash') - const fileData = await new Promise((resolve, reject) => { + const fileData: ArrayBuffer = await new Promise((resolve, reject) => { const fileReader = new FileReader() fileReader.onload = (event) => { - resolve(fileReader.result) + resolve(fileReader.result as ArrayBuffer) } fileReader.onerror = (event) => { reject(fileReader.error) @@ -172,12 +175,11 @@ route.POST('/file', async function (request, h) { console.log('/file/' + hash) return '/file/' + hash } catch (err) { - console.error(err) - return err + return logger(err) } }) -route.GET('/file/{hash}', async function handler (request, h) { +route.GET('/file/{hash}', async function handler (request: ServerRequest, h: Toolkit) { try { const { hash } = request.params const base = pathlib.resolve('data') @@ -189,15 +191,15 @@ route.GET('/file/{hash}', async function handler (request, h) { .header('content-type', 'application/octet-stream') .header('cache-control', 'public,max-age=31536000,immutable') .header('etag', `"${hash}"`) - .header('last-modified', new Date().toGMTString()) + .header('last-modified', new Date().toUTCString()) } catch (err) { - console.log(err) + return logger(err) } }) // SPA routes -route.GET('/assets/{subpath*}', async function handler (request, h) { +route.GET('/assets/{subpath*}', async function handler (request: ServerRequest, h: Toolkit) { try { const { subpath } = request.params console.debug(`GET /assets/${subpath}`) @@ -215,15 +217,14 @@ route.GET('/assets/{subpath*}', async function handler (request, h) { // This should also be suitable for serving unversioned fonts and images. return h.file(pathlib.join(base, subpath)) } catch (err) { - console.error(err) return logger(err) } }) -route.GET('/app/{path*}', function handler (req, h) { +route.GET('/app/{path*}', function (request: ServerRequest, h: Toolkit) { return h.file(pathlib.resolve('./dist/index.html')) }) -route.GET('/', function (req, h) { +route.GET('/', function (request: ServerRequest, h: Toolkit) { return h.redirect('/app/') }) diff --git a/backend/server.ts b/backend/server.ts index eff373715c..7d952035d7 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -1,10 +1,9 @@ -import { blue, bold, gray } from "fmt/colors.ts" +/* globals Deno */ +import { blue, bold, gray } from 'fmt/colors.ts' -import * as http from 'https://deno.land/std@0.132.0/http/server.ts'; -import pogo from 'pogo'; - -import sbp from "@sbp/sbp" -import GiAuth from './auth.ts' +import pogo from 'pogo' +import Toolkit from 'pogo/lib/toolkit.ts' +import sbp from '@sbp/sbp' import './database.ts' import { SERVER_RUNNING } from './events.ts' import { SERVER_INSTANCE, PUBSUB_INSTANCE } from './instance-keys.ts' @@ -13,17 +12,17 @@ import { createNotification, createServer, isUpgradeableRequest, - NOTIFICATION_TYPE, + NOTIFICATION_TYPE } from '~/backend/pubsub.ts' import { router } from './routes.ts' import { GIMessage } from '../shared/domains/chelonia/GIMessage.ts' -const { default: { version }} = await import('~/package.json', { - assert: { type: "json" }, +const { default: { version } } = await import('~/package.json', { + assert: { type: 'json' } }) -const applyPortShift = (env) => { +const applyPortShift = (env: ReturnType) => { // TODO: implement automatic port selection when `PORT_SHIFT` is 'auto'. const API_PORT = 8000 + Number.parseInt(env.PORT_SHIFT || '0') const API_URL = 'http://127.0.0.1:' + API_PORT @@ -35,15 +34,14 @@ const applyPortShift = (env) => { } for (const [key, value] of Object.entries(applyPortShift(Deno.env.toObject()))) { - Deno.env.set(key, value) + Deno.env.set(key as string, value as string) } Deno.env.set('GI_VERSION', `${version}@${new Date().toISOString()}`) -const API_PORT = Deno.env.get('API_PORT') -const API_URL = Deno.env.get('API_URL') +const API_PORT = Deno.env.get('API_PORT') ?? '8000' const CI = Deno.env.get('CI') -const GI_VERSION = Deno.env.get('GI_VERSION') +const GI_VERSION = Deno.env.get('GI_VERSION') as string const NODE_ENV = Deno.env.get('NODE_ENV') ?? 'development' console.info('GI_VERSION:', GI_VERSION) @@ -51,7 +49,7 @@ console.info('NODE_ENV:', NODE_ENV) const pubsub = createServer({ serverHandlers: { - connection (socket: Object, request: Object) { + connection (socket: WebSocket, request: Request) { if (NODE_ENV === 'production') { socket.send(createNotification(NOTIFICATION_TYPE.APP_VERSION, GI_VERSION)) } @@ -62,7 +60,7 @@ const pubsub = createServer({ const pogoServer = pogo.server({ hostname: 'localhost', port: Number.parseInt(API_PORT), - onPreResponse (response, h) { + onPreResponse (response: Response, h: Toolkit) { try { response.headers.set('X-Frame-Options', 'deny') } catch (err) { @@ -75,7 +73,7 @@ const pogoServer = pogo.server({ { const originalInject = pogoServer.inject.bind(pogoServer) - pogoServer.inject = async (request, connInfo) => { + pogoServer.inject = async (request: Request, connInfo: Deno.Conn) => { if (isUpgradeableRequest(request)) { return pubsub.handleUpgradeableRequest(request) } else { @@ -83,7 +81,8 @@ const pogoServer = pogo.server({ // This logging code has to be put here instead of inside onPreResponse // because it requires access to the request object. if (NODE_ENV === 'development' && !CI) { - console.debug(gray(`${connInfo.remoteAddr.hostname}: ${request.method} ${request.url} --> ${response.status}`)) + const { hostname } = connInfo.remoteAddr as Deno.NetAddr + console.debug(gray(`${hostname}: ${request.method} ${request.url} --> ${response.status}`)) } return response } diff --git a/backend/types.ts b/backend/types.ts index 7a82ef0c99..c1c52e10bb 100644 --- a/backend/types.ts +++ b/backend/types.ts @@ -1,6 +1,6 @@ -import Request from './request.ts'; -import ServerResponse from './response.ts'; -import Toolkit from './toolkit.ts'; +import Request from './request.ts' +import ServerResponse from './response.ts' +import Toolkit from './toolkit.ts' export interface Route { method: string, @@ -9,8 +9,8 @@ export interface Route { vhost?: string } -export type RequestParams = { [param: string]: string }; -export type RequestState = { [name: string]: string }; +export type RequestParams = { [param: string]: string } +export type RequestState = { [name: string]: string } export interface RouteOptions extends Omit, 'method' | 'path'> { method?: Route['method'] | Iterable, @@ -26,11 +26,11 @@ export interface MatchedRoute extends NormalizedRoute { params: RequestParams } -type JSONStringifyable = boolean | null | number | object | string; -export type ResponseBody = Deno.Reader | Uint8Array | JSONStringifyable; -export type RouteHandlerResult = ServerResponse | ResponseBody | Error | Promise; -export type RouteHandler = (request: Request, h: Toolkit) => RouteHandlerResult; +type JSONStringifyable = boolean | null | number | object | string +export type ResponseBody = Deno.Reader | Uint8Array | JSONStringifyable +export type RouteHandlerResult = ServerResponse | ResponseBody | Error | Promise +export type RouteHandler = (request: Request, h: Toolkit) => RouteHandlerResult -export type { ServerOptions } from './server.ts'; -export type { FileHandlerOptions } from './helpers/file.ts'; -export type { DirectoryHandlerOptions } from './helpers/directory.tsx'; +export type { ServerOptions } from './server.ts' +export type { FileHandlerOptions } from './helpers/file.ts' +export type { DirectoryHandlerOptions } from './helpers/directory.tsx' diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000000..08d6073831 --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,32 @@ +{ + "parserOptions": { + "parser": "@babel/eslint-parser" + }, + "extends": [ + "plugin:cypress/recommended", + "plugin:flowtype/recommended", + "plugin:vue/essential", + "standard" + ], + "plugins": [ + "cypress", + "flowtype", + "import" + ], + "ignorePatterns": [ + "assets/*", + "model/contracts/misc/flowTyper.js" + ], + "rules": { + "require-await": "error", + "vue/max-attributes-per-line": "off", + "vue/html-indent": "off", + "flowtype/no-types-missing-file-annotation": "off", + "quote-props": "off", + "dot-notation": "off", + "import/extensions": [ + 2, + "ignorePackages" + ] + } +} diff --git a/package-lock.json b/package-lock.json index d46320459d..5de9278b7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,8 @@ "@babel/register": "7.12.1", "@babel/runtime": "7.12.5", "@chelonia/cli": "1.1.2", + "@typescript-eslint/eslint-plugin": "5.37.0", + "@typescript-eslint/parser": "5.37.0", "@vue/component-compiler": "4.2.4", "acorn": "8.0.4", "babel-plugin-module-resolver": "4.1.0", @@ -84,6 +86,7 @@ "sinon": "9.2.1", "stylelint": "13.8.0", "stylelint-config-standard": "20.0.0", + "typescript": "4.8.3", "vue-cli-plugin-pug": "2.0.0", "vue-template-compiler": "2.6.12", "vue-template-es2015-compiler": "1.9.1" @@ -2729,6 +2732,70 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true, + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", + "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.3", "dev": true, @@ -2987,6 +3054,394 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.37.0.tgz", + "integrity": "sha512-Fde6W0IafXktz1UlnhGkrrmnnGpAo1kyX7dnyHHVrmwJOn72Oqm3eYtddrpOwwel2W8PAK9F3pIL5S+lfoM0og==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.37.0", + "@typescript-eslint/type-utils": "5.37.0", + "@typescript-eslint/utils": "5.37.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.37.0.tgz", + "integrity": "sha512-01VzI/ipYKuaG5PkE5+qyJ6m02fVALmMPY3Qq5BHflDx3y4VobbLdHQkSMg9VPRS4KdNt4oYTMaomFoHonBGAw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.37.0", + "@typescript-eslint/types": "5.37.0", + "@typescript-eslint/typescript-estree": "5.37.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.37.0.tgz", + "integrity": "sha512-F67MqrmSXGd/eZnujjtkPgBQzgespu/iCZ+54Ok9X5tALb9L2v3G+QBSoWkXG0p3lcTJsL+iXz5eLUEdSiJU9Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.37.0", + "@typescript-eslint/visitor-keys": "5.37.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.37.0.tgz", + "integrity": "sha512-BSx/O0Z0SXOF5tY0bNTBcDEKz2Ec20GVYvq/H/XNKiUorUFilH7NPbFUuiiyzWaSdN3PA8JV0OvYx0gH/5aFAQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.37.0", + "@typescript-eslint/utils": "5.37.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/types": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.37.0.tgz", + "integrity": "sha512-3frIJiTa5+tCb2iqR/bf7XwU20lnU05r/sgPJnRpwvfZaqCJBrl8Q/mw9vr3NrNdB/XtVyMA0eppRMMBqdJ1bA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.37.0.tgz", + "integrity": "sha512-JkFoFIt/cx59iqEDSgIGnQpCTRv96MQnXCYvJi7QhBC24uyuzbD8wVbajMB1b9x4I0octYFJ3OwjAwNqk1AjDA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.37.0", + "@typescript-eslint/visitor-keys": "5.37.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.37.0.tgz", + "integrity": "sha512-jUEJoQrWbZhmikbcWSMDuUSxEE7ID2W/QCV/uz10WtQqfOuKZUqFGjqLJ+qhDd17rjgp+QJPqTdPIBWwoob2NQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.37.0", + "@typescript-eslint/types": "5.37.0", + "@typescript-eslint/typescript-estree": "5.37.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.37.0.tgz", + "integrity": "sha512-Hp7rT4cENBPIzMwrlehLW/28EVCOcE9U1Z1BQTc8EA8v5qpr7GRGuG+U58V5tTY48zvUOA3KHvw3rA8tY9fbdA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.37.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "dev": true, @@ -7840,25 +8295,26 @@ "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.2.4", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", + "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" + "micromatch": "^4.0.4" }, "engines": { - "node": ">=8" + "node": ">=8.6.0" } }, "node_modules/fast-glob/node_modules/braces": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, - "license": "MIT", "dependencies": { "fill-range": "^7.0.1" }, @@ -7868,8 +8324,9 @@ }, "node_modules/fast-glob/node_modules/fill-range": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -7879,28 +8336,31 @@ }, "node_modules/fast-glob/node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/fast-glob/node_modules/micromatch": { - "version": "4.0.2", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, - "license": "MIT", "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" + "braces": "^3.0.2", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=8" + "node": ">=8.6" } }, "node_modules/fast-glob/node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -8603,15 +9063,16 @@ } }, "node_modules/globby": { - "version": "11.0.1", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, - "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", "slash": "^3.0.0" }, "engines": { @@ -8622,9 +9083,10 @@ } }, "node_modules/globby/node_modules/ignore": { - "version": "5.1.8", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } @@ -9436,9 +9898,10 @@ } }, "node_modules/is-glob": { - "version": "4.0.1", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -11768,9 +12231,10 @@ "license": "MIT" }, "node_modules/picomatch": { - "version": "2.3.0", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -13029,9 +13493,10 @@ } }, "node_modules/regexpp": { - "version": "3.1.0", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -14619,14 +15084,15 @@ "dev": true }, "node_modules/terser": { - "version": "5.12.1", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", + "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", "dev": true, - "license": "BSD-2-Clause", "peer": true, "dependencies": { + "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "bin": { @@ -14700,15 +15166,6 @@ "node": ">=0.4.0" } }, - "node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/text-table": { "version": "0.2.0", "dev": true, @@ -14911,6 +15368,21 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "dev": true, @@ -14966,9 +15438,9 @@ } }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", + "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -18041,6 +18513,61 @@ "version": "1.2.0", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "peer": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "peer": true + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true, + "peer": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", + "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@nodelib/fs.scandir": { "version": "2.1.3", "dev": true, @@ -18243,6 +18770,249 @@ "@types/node": "*" } }, + "@typescript-eslint/eslint-plugin": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.37.0.tgz", + "integrity": "sha512-Fde6W0IafXktz1UlnhGkrrmnnGpAo1kyX7dnyHHVrmwJOn72Oqm3eYtddrpOwwel2W8PAK9F3pIL5S+lfoM0og==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.37.0", + "@typescript-eslint/type-utils": "5.37.0", + "@typescript-eslint/utils": "5.37.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@typescript-eslint/parser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.37.0.tgz", + "integrity": "sha512-01VzI/ipYKuaG5PkE5+qyJ6m02fVALmMPY3Qq5BHflDx3y4VobbLdHQkSMg9VPRS4KdNt4oYTMaomFoHonBGAw==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.37.0", + "@typescript-eslint/types": "5.37.0", + "@typescript-eslint/typescript-estree": "5.37.0", + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.37.0.tgz", + "integrity": "sha512-F67MqrmSXGd/eZnujjtkPgBQzgespu/iCZ+54Ok9X5tALb9L2v3G+QBSoWkXG0p3lcTJsL+iXz5eLUEdSiJU9Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.37.0", + "@typescript-eslint/visitor-keys": "5.37.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.37.0.tgz", + "integrity": "sha512-BSx/O0Z0SXOF5tY0bNTBcDEKz2Ec20GVYvq/H/XNKiUorUFilH7NPbFUuiiyzWaSdN3PA8JV0OvYx0gH/5aFAQ==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "5.37.0", + "@typescript-eslint/utils": "5.37.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/types": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.37.0.tgz", + "integrity": "sha512-3frIJiTa5+tCb2iqR/bf7XwU20lnU05r/sgPJnRpwvfZaqCJBrl8Q/mw9vr3NrNdB/XtVyMA0eppRMMBqdJ1bA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.37.0.tgz", + "integrity": "sha512-JkFoFIt/cx59iqEDSgIGnQpCTRv96MQnXCYvJi7QhBC24uyuzbD8wVbajMB1b9x4I0octYFJ3OwjAwNqk1AjDA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.37.0", + "@typescript-eslint/visitor-keys": "5.37.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@typescript-eslint/utils": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.37.0.tgz", + "integrity": "sha512-jUEJoQrWbZhmikbcWSMDuUSxEE7ID2W/QCV/uz10WtQqfOuKZUqFGjqLJ+qhDd17rjgp+QJPqTdPIBWwoob2NQ==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.37.0", + "@typescript-eslint/types": "5.37.0", + "@typescript-eslint/typescript-estree": "5.37.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.37.0.tgz", + "integrity": "sha512-Hp7rT4cENBPIzMwrlehLW/28EVCOcE9U1Z1BQTc8EA8v5qpr7GRGuG+U58V5tTY48zvUOA3KHvw3rA8tY9fbdA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.37.0", + "eslint-visitor-keys": "^3.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + } + } + }, "@ungap/promise-all-settled": { "version": "1.1.2", "dev": true @@ -21389,19 +22159,22 @@ "dev": true }, "fast-glob": { - "version": "3.2.4", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", + "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" + "micromatch": "^4.0.4" }, "dependencies": { "braces": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, "requires": { "fill-range": "^7.0.1" @@ -21409,6 +22182,8 @@ }, "fill-range": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -21416,18 +22191,24 @@ }, "is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, "micromatch": { - "version": "4.0.2", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" + "braces": "^3.0.2", + "picomatch": "^2.3.1" } }, "to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "requires": { "is-number": "^7.0.0" @@ -21881,19 +22662,23 @@ "dev": true }, "globby": { - "version": "11.0.1", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "requires": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", "slash": "^3.0.0" }, "dependencies": { "ignore": { - "version": "5.1.8", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true } } @@ -22421,7 +23206,9 @@ } }, "is-glob": { - "version": "4.0.1", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -23947,7 +24734,9 @@ "dev": true }, "picomatch": { - "version": "2.3.0", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pify": { @@ -24849,7 +25638,9 @@ } }, "regexpp": { - "version": "3.1.0", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, "regjsgen": { @@ -25938,13 +26729,15 @@ } }, "terser": { - "version": "5.12.1", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", + "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", "dev": true, "peer": true, "requires": { + "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "dependencies": { @@ -25952,11 +26745,6 @@ "version": "8.7.0", "dev": true, "peer": true - }, - "source-map": { - "version": "0.7.3", - "dev": true, - "peer": true } } }, @@ -26124,6 +26912,15 @@ "version": "1.11.1", "dev": true }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tunnel-agent": { "version": "0.6.0", "dev": true, @@ -26160,9 +26957,9 @@ } }, "typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", + "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", "dev": true }, "ua-parser-js": { diff --git a/package.json b/package.json index cb3a8d7613..caf3b01672 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,8 @@ "@babel/register": "7.12.1", "@babel/runtime": "7.12.5", "@chelonia/cli": "1.1.2", + "@typescript-eslint/eslint-plugin": "5.37.0", + "@typescript-eslint/parser": "5.37.0", "@vue/component-compiler": "4.2.4", "acorn": "8.0.4", "babel-plugin-module-resolver": "4.1.0", @@ -170,6 +172,7 @@ "sinon": "9.2.1", "stylelint": "13.8.0", "stylelint-config-standard": "20.0.0", + "typescript": "4.8.3", "vue-cli-plugin-pug": "2.0.0", "vue-template-compiler": "2.6.12", "vue-template-es2015-compiler": "1.9.1" diff --git a/scripts/process-shim.ts b/scripts/process-shim.ts index 1e84ad93c6..697d5439fd 100644 --- a/scripts/process-shim.ts +++ b/scripts/process-shim.ts @@ -1,11 +1,13 @@ -window.process = { +const process = { env: { - get (key) { + get (key: string): string | void { return Deno.env.get(key) }, - set (key, value) { + set (key: string, value: string): void { return Deno.env.set(key, value) } } } -console.log(process); + +// @ts-ignore +globalThis.process = process diff --git a/shared/domains/chelonia/GIMessage.ts b/shared/domains/chelonia/GIMessage.ts index b5ee282a23..6180cbab18 100644 --- a/shared/domains/chelonia/GIMessage.ts +++ b/shared/domains/chelonia/GIMessage.ts @@ -1,7 +1,25 @@ // TODO: rename GIMessage to CMessage or something similar import { blake32Hash } from '~/shared/functions.ts' -import type { JSONType, JSONObject } from '~/shared/types.ts' +import type { JSONObject } from '~/shared/types.ts' + +type JSONType = ReturnType + +type Mapping = { + key: string; + value: string; +} + +type Message = { + version: string; // Semver version string + previousHEAD: string | null; + contractID: string | null; + op: GIOp + manifest: string; + // The nonce makes it difficult to predict message contents + // and makes it easier to prevent conflicts during development. + nonce: number; +} export type GIKeyType = '' @@ -23,10 +41,9 @@ export type GIOpValue = GIOpContract | GIOpActionEncrypted | GIOpActionUnencrypt export type GIOp = [GIOpType, GIOpValue] export class GIMessage { - // flow type annotations to make flow happy - _decrypted: GIOpValue - _mapping: Object - _message: Object + _decrypted?: GIOpValue + _mapping: Mapping + _message: Message static OP_CONTRACT: 'c' = 'c' static OP_ACTION_ENCRYPTED: 'ae' = 'ae' // e2e-encrypted action @@ -43,9 +60,9 @@ export class GIMessage { previousHEAD: string | null = null, op: GIOp, manifest: string, - signatureFn?: Function = defaultSignatureFn - ): this { - const message = { + signatureFn: Function = defaultSignatureFn + ): GIMessage { + const message: Message = { version: '1.0.0', previousHEAD, contractID, @@ -71,7 +88,7 @@ export class GIMessage { } // TODO: we need signature verification upon decryption somewhere... - static deserialize (value: string): this { + static deserialize (value: string): GIMessage { if (!value) throw new Error(`deserialize bad value: ${value}`) return new this({ mapping: { key: blake32Hash(value), value }, @@ -79,7 +96,7 @@ export class GIMessage { }) } - constructor ({ mapping, message }: { mapping: Object, message: Object }) { + constructor ({ mapping, message }: { mapping: Mapping, message: Message }) { this._mapping = mapping this._message = message // perform basic sanity check @@ -107,7 +124,7 @@ export class GIMessage { return this._decrypted } - message (): Object { return this._message } + message (): Message { return this._message } op (): GIOp { return this.message().op } @@ -122,13 +139,13 @@ export class GIMessage { let desc = `` diff --git a/shared/domains/chelonia/chelonia.ts b/shared/domains/chelonia/chelonia.ts index 6c4161dbdc..33e56b35fc 100644 --- a/shared/domains/chelonia/chelonia.ts +++ b/shared/domains/chelonia/chelonia.ts @@ -1,9 +1,11 @@ +declare var process: any + import sbp from '@sbp/sbp' import '@sbp/okturtles.events' import '@sbp/okturtles.eventqueue' import './internals.ts' import { CONTRACTS_MODIFIED, CONTRACT_REGISTERED } from './events.ts' -import { createClient, NOTIFICATION_TYPE } from '~/shared/pubsub.ts' +import { createClient, NOTIFICATION_TYPE, PubsubClient } from '~/shared/pubsub.ts' import { merge, cloneDeep, randomHexString, intersection, difference } from '~/frontend/model/contracts/shared/giLodash.js' import { b64ToStr } from '~/shared/functions.ts' import { handleFetchResult } from '~/frontend/controller/utils/misc.js' @@ -12,16 +14,93 @@ import { GIMessage } from './GIMessage.ts' import { ChelErrorUnrecoverable } from './errors.ts' import type { GIOpContract, GIOpActionUnencrypted } from './GIMessage.ts' -// TODO: define ChelContractType for /defineContract +type JSONType = ReturnType + +export type CheloniaConfig = { + connectionOptions: { + maxRetries: number + reconnectOnTimeout: boolean + timeout: number + } + connectionURL: null | string + contracts: { + defaults: { + modules: Record + exposedGlobals: Record + allowedDomains: string[] + allowedSelectors: string[] + preferSlim: boolean + } + manifests: Record // Contract names -> manifest hashes + overrides: Record // Override default values per-contract. + } + decryptFn: (arg: string) => JSONType + encryptFn: (arg: JSONType) => string + hooks: { + preHandleEvent: null | ((message: GIMessage) => Promise) + postHandleEvent: null | ((message: GIMessage) => Promise) + processError: null | ((e: Error, message: GIMessage) => void) + sideEffectError: null | ((e: Error, message: GIMessage) => void) + handleEventError: null | ((e: Error, message: GIMessage) => void) + syncContractError: null | ((e: Error, contractID: string) => void) + pubsubError: null | ((e: Error, socket: WebSocket) => void) + } + postOp?: (state: any, message: any) => boolean + preOp?: (state: any, message: any) => boolean + reactiveDel: (obj: any, key: string) => void + reactiveSet: (obj: any, key: string, value: any) => typeof value + skipActionProcessing: boolean + skipSideEffects: boolean + skipProcessing?: boolean + stateSelector: string // Override to integrate with, for example, Vuex. + whitelisted: (action: string) => boolean +} + +export type CheloniaInstance = { + config: CheloniaConfig; + contractsModifiedListener?: () => void; + contractSBP?: any; + defContract?: ContractDefinition; + defContractManifest?: string; + defContractSBP?: SBP; + defContractSelectors?: string[]; + manifestToContract: Record; + sideEffectStack: (contractID: string) => any[]; + sideEffectStacks: Record; + state: CheloniaState; + whitelistedActions: Record; +} + +export type CheloniaState = { + contracts: Record; // Contracts we've subscribed to. + pending: string[]; // Prevents processing unexpected data from a malicious server. +} + +export type ContractDefinition = { + actions: any + getters: any + manifest: string + metadata: { + create(): any + validate(meta: any, args: any): void + validate(): void + } + methods: any + name: string + sbp: SBP + state (contractID: string): any +} + +type SBP = (selector: string, ...args: any[]) => any export type ChelRegParams = { contractName: string; server?: string; // TODO: implement! data: Object; hooks?: { - prepublishContract?: (GIMessage) => void; - prepublish?: (GIMessage) => void; - postpublish?: (GIMessage) => void; + prepublishContract?: (msg: GIMessage) => void; + prepublish?: (msg: GIMessage) => void; + postpublish?: (msg: GIMessage) => void; }; publishOptions?: { maxAttempts: number }; } @@ -32,9 +111,9 @@ export type ChelActionParams = { contractID: string; data: Object; hooks?: { - prepublishContract?: (GIMessage) => void; - prepublish?: (GIMessage) => void; - postpublish?: (GIMessage) => void; + prepublishContract?: (msg: GIMessage) => void; + prepublish?: (msg: GIMessage) => void; + postpublish?: (msg: GIMessage) => void; }; publishOptions?: { maxAttempts: number }; } @@ -72,8 +151,8 @@ export default sbp('sbp/selectors/register', { manifests: {} // override! contract names => manifest hashes }, whitelisted: (action: string): boolean => !!this.whitelistedActions[action], - reactiveSet: (obj, key, value) => { obj[key] = value; return value }, // example: set to Vue.set - reactiveDel: (obj, key) => { delete obj[key] }, + reactiveSet: (obj: any, key: string, value: any): typeof value => { obj[key] = value; return value }, // example: set to Vue.set + reactiveDel: (obj: any, key: string): void => { delete obj[key] }, skipActionProcessing: false, skipSideEffects: false, connectionOptions: { @@ -106,10 +185,10 @@ export default sbp('sbp/selectors/register', { return stack } }, - 'chelonia/config': function () { + 'chelonia/config': function (): CheloniaConfig { return cloneDeep(this.config) }, - 'chelonia/configure': async function (config: Object) { + 'chelonia/configure': async function (config: CheloniaConfig) { merge(this.config, config) // merge will strip the hooks off of config.hooks when merging from the root of the object // because they are functions and cloneDeep doesn't clone functions @@ -123,7 +202,7 @@ export default sbp('sbp/selectors/register', { } }, // TODO: allow connecting to multiple servers at once - 'chelonia/connect': function (): Object { + 'chelonia/connect': function (): PubsubClient { if (!this.config.connectionURL) throw new Error('config.connectionURL missing') if (!this.config.connectionOptions) throw new Error('config.connectionOptions missing') if (this.pubsub) { @@ -138,14 +217,14 @@ export default sbp('sbp/selectors/register', { this.pubsub = createClient(pubsubURL, { ...this.config.connectionOptions, messageHandlers: { - [NOTIFICATION_TYPE.ENTRY] (msg) { + [NOTIFICATION_TYPE.ENTRY] (msg: { data: JSONType }) { // We MUST use 'chelonia/private/in/enqueueHandleEvent' to ensure handleEvent() // is called AFTER any currently-running calls to 'chelonia/contract/sync' // to prevent gi.db from throwing "bad previousHEAD" errors. // Calling via SBP also makes it simple to implement 'test/backend.js' sbp('chelonia/private/in/enqueueHandleEvent', GIMessage.deserialize(msg.data)) }, - [NOTIFICATION_TYPE.APP_VERSION] (msg) { + [NOTIFICATION_TYPE.APP_VERSION] (msg: { data: JSONType }) { const ourVersion = process.env.GI_VERSION const theirVersion = msg.data @@ -162,7 +241,7 @@ export default sbp('sbp/selectors/register', { } return this.pubsub }, - 'chelonia/defineContract': function (contract: Object) { + 'chelonia/defineContract': function (contract: ContractDefinition) { if (!ACTION_REGEX.exec(contract.name)) throw new Error(`bad contract name: ${contract.name}`) if (!contract.metadata) contract.metadata = { validate () {}, create: () => ({}) } if (!contract.getters) contract.getters = {} @@ -176,7 +255,7 @@ export default sbp('sbp/selectors/register', { [`${contract.manifest}/${contract.name}/getters`]: () => contract.getters, // 2 ways to cause sideEffects to happen: by defining a sideEffect function in the // contract, or by calling /pushSideEffect w/async SBP call. Can also do both. - [`${contract.manifest}/${contract.name}/pushSideEffect`]: (contractID: string, asyncSbpCall: Array) => { + [`${contract.manifest}/${contract.name}/pushSideEffect`]: (contractID: string, asyncSbpCall: any[]) => { // if this version of the contract is pushing a sideEffect to a function defined by the // contract itself, make sure that it calls the same version of the sideEffect const [sel] = asyncSbpCall @@ -196,7 +275,7 @@ export default sbp('sbp/selectors/register', { // - whatever keys should be passed in as well // base it off of the design of encryptedAction() this.defContractSelectors.push(...sbp('sbp/selectors/register', { - [`${contract.manifest}/${action}/process`]: (message: Object, state: Object) => { + [`${contract.manifest}/${action}/process`]: (message: any, state: any) => { const { meta, data, contractID } = message // TODO: optimize so that you're creating a proxy object only when needed const gProxy = gettersProxy(state, contract.getters) @@ -205,12 +284,13 @@ export default sbp('sbp/selectors/register', { contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) contract.actions[action].process(message, { state, ...gProxy }) }, - [`${contract.manifest}/${action}/sideEffect`]: async (message: Object, state: Object) => { + [`${contract.manifest}/${action}/sideEffect`]: async (message: any, state: any) => { const sideEffects = this.sideEffectStack(message.contractID) while (sideEffects.length > 0) { const sideEffect = sideEffects.shift() try { - await contract.sbp(...sideEffect) + const [selector, ...args] = sideEffect + await contract.sbp(selector, ...args) } catch (e) { console.error(`[chelonia] ERROR: '${e.name}' ${e.message}, for pushed sideEffect of ${message.description()}:`, sideEffect) this.sideEffectStacks[message.contractID] = [] // clear the side effects @@ -278,7 +358,7 @@ export default sbp('sbp/selectors/register', { // This prevents handleEvent getting called with the wrong previousHEAD for an event. return sbp('chelonia/queueInvocation', contractID, [ 'chelonia/private/in/syncContract', contractID - ]).catch((err) => { + ]).catch((err: any) => { console.error(`[chelonia] failed to sync ${contractID}:`, err) throw err // re-throw the error }) @@ -305,43 +385,41 @@ export default sbp('sbp/selectors/register', { // TODO: r.body is a stream.Transform, should we use a callback to process // the events one-by-one instead of converting to giant json object? // however, note if we do that they would be processed in reverse... - 'chelonia/out/eventsSince': async function (contractID: string, since: string) { + 'chelonia/out/eventsSince': async function (contractID: string, since: string): Promise { const events = await fetch(`${this.config.connectionURL}/eventsSince/${contractID}/${since}`) .then(handleFetchResult('json')) if (Array.isArray(events)) { return events.reverse().map(b64ToStr) } }, - 'chelonia/out/latestHash': function (contractID: string) { + 'chelonia/out/latestHash': function (contractID: string): Promise { return fetch(`${this.config.connectionURL}/latestHash/${contractID}`, { cache: 'no-store' }).then(handleFetchResult('text')) }, - 'chelonia/out/eventsBefore': async function (before: string, limit: number) { + 'chelonia/out/eventsBefore': async function (before: string, limit: number): Promise { if (limit <= 0) { console.error('[chelonia] invalid params error: "limit" needs to be positive integer') return } - const events = await fetch(`${this.config.connectionURL}/eventsBefore/${before}/${limit}`) .then(handleFetchResult('json')) if (Array.isArray(events)) { return events.reverse().map(b64ToStr) } }, - 'chelonia/out/eventsBetween': async function (startHash: string, endHash: string, offset: number = 0) { + 'chelonia/out/eventsBetween': async function (startHash: string, endHash: string, offset: number = 0): Promise { if (offset < 0) { console.error('[chelonia] invalid params error: "offset" needs to be positive integer or zero') return } - const events = await fetch(`${this.config.connectionURL}/eventsBetween/${startHash}/${endHash}?offset=${offset}`) .then(handleFetchResult('json')) if (Array.isArray(events)) { return events.reverse().map(b64ToStr) } }, - 'chelonia/latestContractState': async function (contractID: string) { + 'chelonia/latestContractState': async function (contractID: string): Promise { const events = await sbp('chelonia/out/eventsSince', contractID, contractID) let state = {} // fast-path @@ -368,7 +446,7 @@ export default sbp('sbp/selectors/register', { return state }, // 'chelonia/out' - selectors that send data out to the server - 'chelonia/out/registerContract': async function (params: ChelRegParams) { + 'chelonia/out/registerContract': async function (params: ChelRegParams): Promise { const { contractName, hooks, publishOptions } = params const manifestHash = this.config.contracts.manifests[contractName] const contractInfo = this.manifestToContract[manifestHash] @@ -427,9 +505,10 @@ function contractNameFromAction (action: string): string { } async function outEncryptedOrUnencryptedAction ( + this: CheloniaInstance, opType: 'ae' | 'au', params: ChelActionParams -) { +): Promise { const { action, contractID, data, hooks, publishOptions } = params const contractName = contractNameFromAction(action) const manifestHash = this.config.contracts.manifests[contractName] @@ -461,10 +540,10 @@ async function outEncryptedOrUnencryptedAction ( // The only way to pass in the state is by creating a Proxy object that does // that for us. This allows us to maintain compatibility with Vue.js and integrate // the contract getters into the Vue-facing getters. -function gettersProxy (state: Object, getters: Object) { - const proxyGetters = new Proxy({}, { - get (target, prop) { - return getters[prop](state, proxyGetters) +function gettersProxy (state: any, getters: any): { getters: any } { + const proxyGetters: any = new Proxy({}, { + get (target: any, prop: string) { + return (getters[prop] as any)(state, proxyGetters) } }) return { getters: proxyGetters } diff --git a/shared/domains/chelonia/db.ts b/shared/domains/chelonia/db.ts index 0c4d9d27b8..7cbc46b55c 100644 --- a/shared/domains/chelonia/db.ts +++ b/shared/domains/chelonia/db.ts @@ -1,3 +1,5 @@ +declare var process: any + import sbp from '@sbp/sbp' import '@sbp/okturtles.data' import '@sbp/okturtles.eventqueue' @@ -13,37 +15,38 @@ sbp('sbp/selectors/unsafe', ['chelonia/db/get', 'chelonia/db/set', 'chelonia/db/ const dbPrimitiveSelectors = process.env.LIGHTWEIGHT_CLIENT === 'true' ? { - 'chelonia/db/get': function (key) { + 'chelonia/db/get': function (key: string): Promise { const id = sbp('chelonia/db/contractIdFromLogHEAD', key) + // @ts-ignore Property 'config' does not exist. return Promise.resolve(id ? sbp(this.config.stateSelector).contracts[id]?.HEAD : null) }, - 'chelonia/db/set': function (key, value) { return Promise.resolve(value) }, - 'chelonia/db/delete': function () { return Promise.resolve() } + 'chelonia/db/set': function (key: string, value: any): Promise { return Promise.resolve(value) }, + 'chelonia/db/delete': function (): Promise { return Promise.resolve() } } : { - 'chelonia/db/get': function (key) { + 'chelonia/db/get': function (key: any): any { return Promise.resolve(sbp('okTurtles.data/get', key)) }, - 'chelonia/db/set': function (key, value) { + 'chelonia/db/set': function (key: any, value: any) { return Promise.resolve(sbp('okTurtles.data/set', key, value)) }, - 'chelonia/db/delete': function (key) { + 'chelonia/db/delete': function (key: any) { return Promise.resolve(sbp('okTurtles.data/delete', key)) } } export default (sbp('sbp/selectors/register', { ...dbPrimitiveSelectors, - 'chelonia/db/logHEAD': function (contractID) { + 'chelonia/db/logHEAD': function (contractID: string) { return `${contractID}${headSuffix}` }, - 'chelonia/db/contractIdFromLogHEAD': function (key) { + 'chelonia/db/contractIdFromLogHEAD': function (key: string) { return key.endsWith(headSuffix) ? key.slice(0, -headSuffix.length) : null }, - 'chelonia/db/latestHash': function (contractID) { + 'chelonia/db/latestHash': function (contractID: string) { return sbp('chelonia/db/get', sbp('chelonia/db/logHEAD', contractID)) }, - 'chelonia/db/getEntry': async function (hash) { + 'chelonia/db/getEntry': async function (hash: string) { try { const value = await sbp('chelonia/db/get', hash) if (!value) throw new Error(`no entry for ${hash}!`) @@ -52,7 +55,7 @@ export default (sbp('sbp/selectors/register', { throw new ChelErrorDBConnection(`${e.name} during getEntry: ${e.message}`) } }, - 'chelonia/db/addEntry': function (entry) { + 'chelonia/db/addEntry': function (entry: GIMessage) { // because addEntry contains multiple awaits - we want to make sure it gets executed // "atomically" to minimize the chance of a contract fork return sbp('okTurtles.eventQueue/queueEvent', `chelonia/db/${entry.contractID()}`, [ @@ -60,7 +63,8 @@ export default (sbp('sbp/selectors/register', { ]) }, // NEVER call this directly yourself! _always_ call 'chelonia/db/addEntry' instead - 'chelonia/private/db/addEntry': async function (entry) { + // @throws ChelErrorDBConnection, ChelErrorDBConnection + 'chelonia/private/db/addEntry': async function (entry: GIMessage): Promise { try { const { previousHEAD } = entry.message() const contractID = entry.contractID() @@ -84,7 +88,7 @@ export default (sbp('sbp/selectors/register', { throw new ChelErrorDBConnection(`${e.name} during addEntry: ${e.message}`) } }, - 'chelonia/db/lastEntry': async function (contractID) { + 'chelonia/db/lastEntry': async function (contractID: string) { try { const hash = await sbp('chelonia/db/latestHash', contractID) if (!hash) throw new Error(`contract ${contractID} has no latest hash!`) diff --git a/shared/domains/chelonia/errors.ts b/shared/domains/chelonia/errors.ts index e19d112030..e4a0d25652 100644 --- a/shared/domains/chelonia/errors.ts +++ b/shared/domains/chelonia/errors.ts @@ -1,7 +1,7 @@ export class ChelErrorDBBadPreviousHEAD extends Error { // ugly boilerplate because JavaScript is stupid // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types - constructor (...params) { + constructor (...params: any[]) { super(...params) // this.name = this.constructor.name this.name = 'ChelErrorDBBadPreviousHEAD' // string literal so minifier doesn't overwrite @@ -11,7 +11,7 @@ export class ChelErrorDBBadPreviousHEAD extends Error { } } export class ChelErrorDBConnection extends Error { - constructor (...params) { + constructor (...params: any[]) { super(...params) this.name = 'ChelErrorDBConnection' if (Error.captureStackTrace) { @@ -21,7 +21,7 @@ export class ChelErrorDBConnection extends Error { } export class ChelErrorUnexpected extends Error { - constructor (...params) { + constructor (...params: any[]) { super(...params) this.name = 'ChelErrorUnexpected' if (Error.captureStackTrace) { @@ -31,7 +31,7 @@ export class ChelErrorUnexpected extends Error { } export class ChelErrorUnrecoverable extends Error { - constructor (...params) { + constructor (...params: any[]) { super(...params) this.name = 'ChelErrorUnrecoverable' if (Error.captureStackTrace) { diff --git a/shared/domains/chelonia/internals.ts b/shared/domains/chelonia/internals.ts index d856afb893..342b5bb8df 100644 --- a/shared/domains/chelonia/internals.ts +++ b/shared/domains/chelonia/internals.ts @@ -1,6 +1,16 @@ import sbp, { domainFromSelector } from '@sbp/sbp' import './db.ts' -import { GIMessage } from './GIMessage.ts' +import { + GIMessage, + GIOpActionEncrypted, + GIOpActionUnencrypted, + GIOpContract, + GIOpKeyAdd, + GIOpPropSet, + GIOpValue +} from './GIMessage.ts' +import type { CheloniaConfig, CheloniaInstance, CheloniaState, ContractDefinition } from './chelonia.ts' + import { randomIntFromRange, delay, cloneDeep, debounce, pick } from '~/frontend/model/contracts/shared/giLodash.js' import { ChelErrorUnexpected, ChelErrorUnrecoverable } from './errors.ts' import { CONTRACT_IS_SYNCING, CONTRACTS_MODIFIED, EVENT_HANDLED } from './events.ts' @@ -12,10 +22,10 @@ import { blake32Hash } from '~/shared/functions.ts' export default sbp('sbp/selectors/register', { // DO NOT CALL ANY OF THESE YOURSELF! - 'chelonia/private/state': function () { + 'chelonia/private/state': function (this: CheloniaInstance): CheloniaState { return this.state }, - 'chelonia/private/loadManifest': async function (manifestHash) { + 'chelonia/private/loadManifest': async function (this: CheloniaInstance, manifestHash: string) { if (this.manifestToContract[manifestHash]) { console.warn('[chelonia]: already loaded manifest', manifestHash) return @@ -31,14 +41,14 @@ export default sbp('sbp/selectors/register', { if (sourceHash !== contractInfo.hash) { throw new Error(`bad hash ${sourceHash} for contract '${contractInfo.file}'! Should be: ${contractInfo.hash}`) } - function reduceAllow (acc, v) { acc[v] = true; return acc } + function reduceAllow (acc: Record, v: string) { acc[v] = true; return acc } const allowedSels = ['okTurtles.events/on', 'chelonia/defineContract'] .concat(this.config.contracts.defaults.allowedSelectors) .reduce(reduceAllow, {}) const allowedDoms = this.config.contracts.defaults.allowedDomains .reduce(reduceAllow, {}) - let contractName // eslint-disable-line prefer-const - const contractSBP = (selector, ...args) => { + let contractName: string // eslint-disable-line prefer-const + const contractSBP = (selector: string, ...args: any[]) => { const domain = domainFromSelector(selector) if (selector.startsWith(contractName)) { selector = `${manifestHash}/${selector}` @@ -98,15 +108,15 @@ export default sbp('sbp/selectors/register', { parseInt, Promise, ...this.config.contracts.defaults.exposedGlobals, - require: (dep) => { + require: (dep: string) => { return dep === '@sbp/sbp' ? contractSBP : this.config.contracts.defaults.modules[dep] }, sbp: contractSBP }) - contractName = this.defContract.name - this.defContractSelectors.forEach(s => { allowedSels[s] = true }) + contractName = (this.defContract as ContractDefinition).name + ;(this.defContractSelectors as string[]).forEach((s: string) => { allowedSels[s] = true }) this.manifestToContract[manifestHash] = { slim: contractInfo === body.contractSlim, info: contractInfo, @@ -115,7 +125,7 @@ export default sbp('sbp/selectors/register', { }, // used by, e.g. 'chelonia/contract/wait' 'chelonia/private/noop': function () {}, - 'chelonia/private/out/publishEvent': async function (entry, { maxAttempts = 3 } = {}) { + 'chelonia/private/out/publishEvent': async function (entry: GIMessage, { maxAttempts = 3 } = {}) { const contractID = entry.contractID() let attempt = 1 // auto resend after short random delay @@ -154,7 +164,7 @@ export default sbp('sbp/selectors/register', { } } }, - 'chelonia/private/in/processMessage': async function (message, state) { + 'chelonia/private/in/processMessage': async function (this: CheloniaInstance, message: GIMessage, state: any) { const [opT, opV] = message.op() const hash = message.hash() const contractID = message.contractID() @@ -162,19 +172,19 @@ export default sbp('sbp/selectors/register', { const config = this.config if (!state._vm) state._vm = {} const opFns = { - [GIMessage.OP_CONTRACT] (v) { + [GIMessage.OP_CONTRACT] (v: GIOpContract) { // TODO: shouldn't each contract have its own set of authorized keys? if (!state._vm.authorizedKeys) state._vm.authorizedKeys = [] // TODO: we probably want to be pushing the de-JSON-ified key here state._vm.authorizedKeys.push({ key: v.keyJSON, context: 'owner' }) }, - [GIMessage.OP_ACTION_ENCRYPTED] (v) { + [GIMessage.OP_ACTION_ENCRYPTED] (v: GIOpActionEncrypted) { if (!config.skipActionProcessing) { const decrypted = message.decryptedValue(config.decryptFn) opFns[GIMessage.OP_ACTION_UNENCRYPTED](decrypted) } }, - [GIMessage.OP_ACTION_UNENCRYPTED] (v) { + [GIMessage.OP_ACTION_UNENCRYPTED] (v: GIOpActionUnencrypted) { if (!config.skipActionProcessing) { const { data, meta, action } = v if (!config.whitelisted(action)) { @@ -184,11 +194,11 @@ export default sbp('sbp/selectors/register', { } }, [GIMessage.OP_PROP_DEL]: notImplemented, - [GIMessage.OP_PROP_SET] (v) { + [GIMessage.OP_PROP_SET] (v: GIOpPropSet) { if (!state._vm.props) state._vm.props = {} state._vm.props[v.key] = v.value }, - [GIMessage.OP_KEY_ADD] (v) { + [GIMessage.OP_KEY_ADD] (v: GIOpKeyAdd) { // TODO: implement this. consider creating a function so that // we're not duplicating code in [GIMessage.OP_CONTRACT] // if (!state._vm.authorizedKeys) state._vm.authorizedKeys = [] @@ -204,16 +214,18 @@ export default sbp('sbp/selectors/register', { if (config.preOp) { processOp = config.preOp(message, state) !== false && processOp } - if (config[`preOp_${opT}`]) { - processOp = config[`preOp_${opT}`](message, state) !== false && processOp + if (`preOp_${opT}` in config) { + // @ts-ignore Property 'preOp_ae' does not exist on type 'CheloniaConfig' + processOp = (config[`preOp_${opT}`] as any)(message, state) !== false && processOp } if (processOp && !config.skipProcessing) { opFns[opT](opV) config.postOp && config.postOp(message, state) - config[`postOp_${opT}`] && config[`postOp_${opT}`](message, state) + // @ts-ignore Property 'postOp_ae' does not exist on type 'CheloniaConfig' + ;(`postOp_${opT}` in config) && (config[`postOp_${opT}`] as any)(message, state) } }, - 'chelonia/private/in/enqueueHandleEvent': function (event) { + 'chelonia/private/in/enqueueHandleEvent': function (this: CheloniaInstance, event: GIMessage) { // make sure handleEvent is called AFTER any currently-running invocations // to 'chelonia/contract/sync', to prevent gi.db from throwing // "bad previousHEAD" errors @@ -221,7 +233,7 @@ export default sbp('sbp/selectors/register', { 'chelonia/private/in/handleEvent', event ]) }, - 'chelonia/private/in/syncContract': async function (contractID) { + 'chelonia/private/in/syncContract': async function (this: CheloniaInstance, contractID: string) { const state = sbp(this.config.stateSelector) const latest = await sbp('chelonia/out/latestHash', contractID) console.debug(`[chelonia] syncContract: ${contractID} latestHash is: ${latest}`) @@ -259,7 +271,7 @@ export default sbp('sbp/selectors/register', { throw e } }, - 'chelonia/private/in/handleEvent': async function (message) { + 'chelonia/private/in/handleEvent': async function (this: CheloniaInstance, message: GIMessage) { const state = sbp(this.config.stateSelector) const contractID = message.contractID() const hash = message.hash() @@ -327,11 +339,11 @@ export default sbp('sbp/selectors/register', { } }) -const eventsToReinjest = [] -const reprocessDebounced = debounce((contractID) => sbp('chelonia/contract/sync', contractID), 1000) +const eventsToReinjest: string[] = [] +const reprocessDebounced = debounce((contractID: string) => sbp('chelonia/contract/sync', contractID), 1000, undefined) const handleEvent = { - async addMessageToDB (message) { + async addMessageToDB (message: GIMessage): Promise { const contractID = message.contractID() const hash = message.hash() try { @@ -360,14 +372,13 @@ const handleEvent = { throw e } }, - async processMutation (message, state) { + async processMutation (this: CheloniaInstance, message: GIMessage, state: any) { const contractID = message.contractID() if (message.isFirstMessage()) { - // Flow doesn't understand that a first message must be a contract, - // so we have to help it a bit in order to acces the 'type' property. - const { type } = ((message.opValue())) + const { type } = message.opValue() as GIOpContract if (!state[contractID]) { console.debug(`contract ${type} registered for ${contractID}`) + ;(this.config as CheloniaConfig) this.config.reactiveSet(state, contractID, {}) this.config.reactiveSet(state.contracts, contractID, { type, HEAD: contractID }) } @@ -378,8 +389,8 @@ const handleEvent = { } await sbp('chelonia/private/in/processMessage', message, state[contractID]) }, - async processSideEffects (message) { - if ([GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_ACTION_UNENCRYPTED].includes(message.opType())) { + async processSideEffects (this: CheloniaInstance, message: GIMessage) { + if (([GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_ACTION_UNENCRYPTED] as string[]).includes(message.opType())) { const contractID = message.contractID() const manifestHash = message.manifest() const hash = message.hash() @@ -388,7 +399,7 @@ const handleEvent = { await sbp(`${manifestHash}/${action}/sideEffect`, mutation) } }, - revertProcess ({ message, state, contractID, contractStateCopy }) { + revertProcess (this: CheloniaInstance, { message, state, contractID, contractStateCopy }: any) { console.warn(`[chelonia] reverting mutation ${message.description()}: ${message.serialize()}. Any side effects will be skipped!`) if (!contractStateCopy) { console.warn(`[chelonia] mutation reversion on very first message for contract ${contractID}! Your contract may be too damaged to be useful and should be redeployed with bugfixes.`) @@ -396,7 +407,7 @@ const handleEvent = { } this.config.reactiveSet(state, contractID, contractStateCopy) }, - revertSideEffect ({ message, state, contractID, contractStateCopy, stateCopy }) { + revertSideEffect (this: CheloniaInstance, { message, state, contractID, contractStateCopy, stateCopy }: any) { console.warn(`[chelonia] reverting entire state because failed sideEffect for ${message.description()}: ${message.serialize()}`) if (!contractStateCopy) { this.config.reactiveDel(state, contractID) @@ -409,7 +420,7 @@ const handleEvent = { } } -const notImplemented = (v) => { +const notImplemented = (v: any) => { throw new Error(`chelonia: action not implemented to handle: ${JSON.stringify(v)}.`) } diff --git a/shared/functions.ts b/shared/functions.ts index 683e50c36f..5e3ed29387 100644 --- a/shared/functions.ts +++ b/shared/functions.ts @@ -7,12 +7,14 @@ import blake from 'blakejs' import { Buffer } from 'buffer' if (typeof window === 'object') { + // @ts-ignore window.Buffer = Buffer } else { - global.Buffer = Buffer + // @ts-ignore + globalThis.Buffer = Buffer } -export function blake32Hash (data) { +export function blake32Hash (data: any) { // TODO: for node/electron, switch to: https://github.com/ludios/node-blake2 const uint8array = blake.blake2b(data, null, 32) // TODO: if we switch to webpack we may need: https://github.com/feross/buffer @@ -27,15 +29,15 @@ export function blake32Hash (data) { // and you have to jump through some hoops to get it to work: // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa#Unicode_strings // These hoops might result in inconsistencies between Node.js and the frontend. -export const b64ToBuf = (b64) => Buffer.from(b64, 'base64') -export const b64ToStr = (b64) => b64ToBuf(b64).toString('utf8') -export const bufToB64 = (buf) => Buffer.from(buf).toString('base64') -export const strToBuf = (str) => Buffer.from(str, 'utf8') -export const strToB64 = (str) => strToBuf(str).toString('base64') -export const bytesToB64 = (ary) => Buffer.from(ary).toString('base64') +export const b64ToBuf = (b64: string) => Buffer.from(b64, 'base64') +export const b64ToStr = (b64: string) => b64ToBuf(b64).toString('utf8') +export const bufToB64 = (buf: any) => Buffer.from(buf).toString('base64') +export const strToBuf = (str: string) => Buffer.from(str, 'utf8') +export const strToB64 = (str: string) => strToBuf(str).toString('base64') +export const bytesToB64 = (ary: any) => Buffer.from(ary).toString('base64') export function sign ( - { publicKey, secretKey }, + { publicKey, secretKey }: any, msg = 'hello!', futz = '' ) { @@ -47,7 +49,7 @@ export function sign ( } export function verify ( - msg, key, sig + msg: string, key: string, sig: string ) { return nacl.sign.detached.verify(strToBuf(msg), b64ToBuf(sig), b64ToBuf(key)) } diff --git a/shared/pubsub.ts b/shared/pubsub.ts index 000170c902..c2c854f1c2 100644 --- a/shared/pubsub.ts +++ b/shared/pubsub.ts @@ -1,8 +1,17 @@ -'use strict' +declare var process: any import sbp from '@sbp/sbp' import '@sbp/okturtles.events' +type JSONType = ReturnType; + +// ====== Types ====== // + +type Message = { + [key: string]: JSONType; + type: string +} + // ====== Event name constants ====== // export const PUBSUB_ERROR = 'pubsub-error' @@ -11,18 +20,6 @@ export const PUBSUB_RECONNECTION_FAILED = 'pubsub-reconnection-failed' export const PUBSUB_RECONNECTION_SCHEDULED = 'pubsub-reconnection-scheduled' export const PUBSUB_RECONNECTION_SUCCEEDED = 'pubsub-reconnection-succeeded' -// ====== Types ====== // - -/* - * Flowtype usage notes: - * - * - The '+' prefix indicates properties that should not be re-assigned or - * deleted after their initialization. - * - * - 'TimeoutID' is an opaque type declared in Flow's core definition file, - * used as the return type of the core setTimeout() function. - */ - // ====== Enums ====== // export const NOTIFICATION_TYPE = Object.freeze({ @@ -46,6 +43,240 @@ export const RESPONSE_TYPE = Object.freeze({ SUCCESS: 'success' }) +export class PubsubClient { + connectionTimeoutID?: number; + customEventHandlers: Record; + // The current number of connection attempts that failed. + // Reset to 0 upon successful connection. + // Used to compute how long to wait before the next reconnection attempt. + failedConnectionAttempts: number; + isLocal: boolean; + // True if this client has never been connected yet. + isNew: boolean; + listeners: Record; + messageHandlers: Record void>; + nextConnectionAttemptDelayID?: number; + options: any; + // Requested subscriptions for which we didn't receive a response yet. + pendingSubscriptionSet: Set; + pendingSyncSet: Set; + pendingUnsubscriptionSet: Set; + pingTimeoutID?: number; + shouldReconnect: boolean; + // The underlying WebSocket object. + // A new one is necessary for every connection or reconnection attempt. + socket: WebSocket | null = null; + subscriptionSet: Set; + url: string; + + constructor (url: string, options: any = {}) { + this.customEventHandlers = options.handlers ?? {} + this.failedConnectionAttempts = 0 + this.isLocal = /\/\/(localhost|127\.0\.0\.1)([:?/]|$)/.test(url) + // True if this client has never been connected yet. + this.isNew = true + this.listeners = Object.create(null) + this.messageHandlers = { ...defaultMessageHandlers, ...options.messageHandlers } + this.options = { ...defaultOptions, ...options } + // Requested subscriptions for which we didn't receive a response yet. + this.pendingSubscriptionSet = new Set() + this.pendingSyncSet = new Set() + this.pendingUnsubscriptionSet = new Set() + this.shouldReconnect = true + this.subscriptionSet = new Set() + this.url = url.replace(/^http/, 'ws') + + const client = this + // Create and save references to reusable event listeners. + // Every time a new underlying WebSocket object will be created for this + // client instance, these event listeners will be detached from the older + // socket then attached to the new one, hereby avoiding both unnecessary + // allocations and garbage collections of a bunch of functions every time. + // Another benefit is the ability to patch the client protocol at runtime by + // updating the client's custom event handler map. + for (const name of Object.keys(defaultClientEventHandlers)) { + client.listeners[name] = (event: any) => { + try { + // Use `.call()` to pass the client via the 'this' binding. + // @ts-ignore + defaultClientEventHandlers[name as SocketEventName]?.call(client, event) + client.customEventHandlers[name]?.call(client, event) + } catch (error) { + // Do not throw any error but emit an `error` event instead. + sbp('okTurtles.events/emit', PUBSUB_ERROR, client, error.message) + } + } + } + // Add global event listeners before the first connection. + if (typeof window === 'object') { + for (const name of globalEventNames) { + window.addEventListener(name, client.listeners[name]) + } + } + if (!client.options.manual) { + client.connect() + } + } + + clearAllTimers () { + clearTimeout(this.connectionTimeoutID) + clearTimeout(this.nextConnectionAttemptDelayID) + clearTimeout(this.pingTimeoutID) + this.connectionTimeoutID = undefined + this.nextConnectionAttemptDelayID = undefined + this.pingTimeoutID = undefined + } + + // Performs a connection or reconnection attempt. + connect () { + const client = this + + if (client.socket !== null) { + throw new Error('connect() can only be called if there is no current socket.') + } + if (client.nextConnectionAttemptDelayID) { + throw new Error('connect() must not be called during a reconnection delay.') + } + if (!client.shouldReconnect) { + throw new Error('connect() should no longer be called on this instance.') + } + client.socket = new WebSocket(client.url) + + if (client.options.timeout) { + client.connectionTimeoutID = setTimeout(() => { + client.connectionTimeoutID = undefined + client.socket?.close(4000, 'timeout') + }, client.options.timeout) + } + // Attach WebSocket event listeners. + for (const name of socketEventNames) { + client.socket.addEventListener(name, client.listeners[name]) + } + } + + /** + * Immediately close the socket, stop listening for events and clear any cache. + * + * This method is used in unit tests. + * - In particular, no 'close' event handler will be called. + * - Any incoming or outgoing buffered data will be discarded. + * - Any pending messages will be discarded. + */ + destroy () { + const client = this + + client.clearAllTimers() + // Update property values. + // Note: do not clear 'client.options'. + client.pendingSubscriptionSet.clear() + client.pendingUnsubscriptionSet.clear() + client.subscriptionSet.clear() + // Remove global event listeners. + if (typeof window === 'object') { + for (const name of globalEventNames) { + window.removeEventListener(name, client.listeners[name]) + } + } + // Remove WebSocket event listeners. + if (client.socket) { + for (const name of socketEventNames) { + client.socket.removeEventListener(name, client.listeners[name]) + } + client.socket.close(4001, 'destroy') + } + client.listeners = {} + client.socket = null + client.shouldReconnect = false + } + + getNextRandomDelay (): number { + const client = this + + const { + maxReconnectionDelay, + minReconnectionDelay, + reconnectionDelayGrowFactor + } = client.options + + const minDelay = minReconnectionDelay * reconnectionDelayGrowFactor ** client.failedConnectionAttempts + const maxDelay = minDelay * reconnectionDelayGrowFactor + + return Math.min(maxReconnectionDelay, Math.round(minDelay + Math.random() * (maxDelay - minDelay))) + } + + // Schedules a connection attempt to happen after a delay computed according to + // a randomized exponential backoff algorithm variant. + scheduleConnectionAttempt () { + const client = this + + if (!client.shouldReconnect) { + throw new Error('Cannot call `scheduleConnectionAttempt()` when `shouldReconnect` is false.') + } + if (client.nextConnectionAttemptDelayID) { + return console.warn('[pubsub] A reconnection attempt is already scheduled.') + } + const delay = client.getNextRandomDelay() + const nth = client.failedConnectionAttempts + 1 + + client.nextConnectionAttemptDelayID = setTimeout(() => { + sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_ATTEMPT, client) + client.nextConnectionAttemptDelayID = undefined + client.connect() + }, delay) + sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_SCHEDULED, client, { delay, nth }) + } + + // Unused for now. + pub (contractID: string, data: JSONType) { + } + + /** + * Sends a SUB request to the server as soon as possible. + * + * - The given contract ID will be cached until we get a relevant server + * response, allowing us to resend the same request if necessary. + * - Any identical UNSUB request that has not been sent yet will be cancelled. + * - Calling this method again before the server has responded has no effect. + * @param contractID - The ID of the contract whose updates we want to subscribe to. + */ + sub (contractID: string, dontBroadcast = false) { + const client = this + const { socket } = this + + if (!client.pendingSubscriptionSet.has(contractID)) { + client.pendingSubscriptionSet.add(contractID) + client.pendingUnsubscriptionSet.delete(contractID) + + if (socket?.readyState === WebSocket.OPEN) { + socket.send(createRequest(REQUEST_TYPE.SUB, { contractID }, dontBroadcast)) + } + } + } + + /** + * Sends an UNSUB request to the server as soon as possible. + * + * - The given contract ID will be cached until we get a relevant server + * response, allowing us to resend the same request if necessary. + * - Any identical SUB request that has not been sent yet will be cancelled. + * - Calling this method again before the server has responded has no effect. + * @param contractID - The ID of the contract whose updates we want to unsubscribe from. + */ + unsub (contractID: string, dontBroadcast = false) { + const client = this + const { socket } = this + + if (!client.pendingUnsubscriptionSet.has(contractID)) { + client.pendingSubscriptionSet.delete(contractID) + client.pendingUnsubscriptionSet.add(contractID) + + if (socket?.readyState === WebSocket.OPEN) { + socket.send(createRequest(REQUEST_TYPE.UNSUB, { contractID }, dontBroadcast)) + } + } + } +} + // ====== API ====== // /** @@ -65,70 +296,15 @@ export const RESPONSE_TYPE = Object.freeze({ * {number?} timeout=5_000 - Connection timeout duration in milliseconds. * @returns {PubSubClient} */ -export function createClient (url, options = {}) { - const client = { - customEventHandlers: options.handlers || {}, - // The current number of connection attempts that failed. - // Reset to 0 upon successful connection. - // Used to compute how long to wait before the next reconnection attempt. - failedConnectionAttempts: 0, - isLocal: /\/\/(localhost|127\.0\.0\.1)([:?/]|$)/.test(url), - // True if this client has never been connected yet. - isNew: true, - listeners: Object.create(null), - messageHandlers: { ...defaultMessageHandlers, ...options.messageHandlers }, - nextConnectionAttemptDelayID: undefined, - options: { ...defaultOptions, ...options }, - // Requested subscriptions for which we didn't receive a response yet. - pendingSubscriptionSet: new Set(), - pendingSyncSet: new Set(), - pendingUnsubscriptionSet: new Set(), - pingTimeoutID: undefined, - shouldReconnect: true, - // The underlying WebSocket object. - // A new one is necessary for every connection or reconnection attempt. - socket: null, - subscriptionSet: new Set(), - connectionTimeoutID: undefined, - url: url.replace(/^http/, 'ws'), - ...publicMethods - } - // Create and save references to reusable event listeners. - // Every time a new underlying WebSocket object will be created for this - // client instance, these event listeners will be detached from the older - // socket then attached to the new one, hereby avoiding both unnecessary - // allocations and garbage collections of a bunch of functions every time. - // Another benefit is the ability to patch the client protocol at runtime by - // updating the client's custom event handler map. - for (const name of Object.keys(defaultClientEventHandlers)) { - client.listeners[name] = (event) => { - try { - // Use `.call()` to pass the client via the 'this' binding. - defaultClientEventHandlers[name]?.call(client, event) - client.customEventHandlers[name]?.call(client, event) - } catch (error) { - // Do not throw any error but emit an `error` event instead. - sbp('okTurtles.events/emit', PUBSUB_ERROR, client, error.message) - } - } - } - // Add global event listeners before the first connection. - if (typeof window === 'object') { - for (const name of globalEventNames) { - window.addEventListener(name, client.listeners[name]) - } - } - if (!client.options.manual) { - client.connect() - } - return client +export function createClient (url: string, options: any = {}): PubsubClient { + return new PubsubClient(url, options) } -export function createMessage (type, data) { +export function createMessage (type: string, data: JSONType): string { return JSON.stringify({ type, data }) } -export function createRequest (type, data, dontBroadcast = false) { +export function createRequest (type: string, data: JSONType, dontBroadcast = false): string { // Had to use Object.assign() instead of object spreading to make Flow happy. return JSON.stringify(Object.assign({ type, dontBroadcast }, data)) } @@ -136,7 +312,7 @@ export function createRequest (type, data, dontBroadcast = false) { // These handlers receive the PubSubClient instance through the `this` binding. const defaultClientEventHandlers = { // Emitted when the connection is closed. - close (event) { + close (this: PubsubClient, event: CloseEvent) { const client = this console.debug('[pubsub] Event: close', event.code, event.reason) @@ -193,7 +369,7 @@ const defaultClientEventHandlers = { // Emitted when an error has occured. // The socket will be closed automatically by the engine if necessary. - error (event) { + error (this: PubsubClient, event: Event) { const client = this // Not all error events should be logged with console.error, for example every // failed connection attempt generates one such event. @@ -204,7 +380,7 @@ const defaultClientEventHandlers = { // Emitted when a message is received. // The connection will be terminated if the message is malformed or has an // unexpected data type (e.g. binary instead of text). - message (event) { + message (this: PubsubClient, event: MessageEvent) { const client = this const { data } = event @@ -233,7 +409,7 @@ const defaultClientEventHandlers = { } }, - offline (event) { + offline (this: PubsubClient, event: Event) { console.info('[pubsub] Event: offline') const client = this @@ -244,7 +420,7 @@ const defaultClientEventHandlers = { client.socket?.close(4002, 'offline') }, - online (event) { + online (this: PubsubClient, event: Event) { console.info('[pubsub] Event: online') const client = this @@ -257,7 +433,7 @@ const defaultClientEventHandlers = { }, // Emitted when the connection is established. - open (event) { + open (this: PubsubClient, event: Event) { console.debug('[pubsub] Event: open') const client = this const { options } = this @@ -288,22 +464,22 @@ const defaultClientEventHandlers = { // There should be no pending unsubscription since we just got connected. }, - 'reconnection-attempt' (event) { + 'reconnection-attempt' (this: PubsubClient, event: CustomEvent) { console.info('[pubsub] Trying to reconnect...') }, - 'reconnection-succeeded' (event) { + 'reconnection-succeeded' (this: PubsubClient, event: CustomEvent) { console.info('[pubsub] Connection re-established') }, - 'reconnection-failed' (event) { + 'reconnection-failed' (this: PubsubClient, event: CustomEvent) { console.warn('[pubsub] Reconnection failed') const client = this client.destroy() }, - 'reconnection-scheduled' (event) { + 'reconnection-scheduled' (event: CustomEvent) { const { delay, nth } = event.detail console.info(`[pubsub] Scheduled connection attempt ${nth} in ~${delay} ms`) } @@ -311,11 +487,11 @@ const defaultClientEventHandlers = { // These handlers receive the PubSubClient instance through the `this` binding. const defaultMessageHandlers = { - [NOTIFICATION_TYPE.ENTRY] (msg) { + [NOTIFICATION_TYPE.ENTRY] (this: PubsubClient, msg: Message) { console.debug('[pubsub] Received ENTRY:', msg) }, - [NOTIFICATION_TYPE.PING] ({ data }) { + [NOTIFICATION_TYPE.PING] (this: PubsubClient, { data }: Message) { const client = this if (client.options.logPingMessages) { @@ -331,19 +507,19 @@ const defaultMessageHandlers = { }, // PUB can be used to send ephemeral messages outside of any contract log. - [NOTIFICATION_TYPE.PUB] (msg) { + [NOTIFICATION_TYPE.PUB] (msg: Message) { console.debug(`[pubsub] Ignoring ${msg.type} message:`, msg.data) }, - [NOTIFICATION_TYPE.SUB] (msg) { + [NOTIFICATION_TYPE.SUB] (msg: Message) { console.debug(`[pubsub] Ignoring ${msg.type} message:`, msg.data) }, - [NOTIFICATION_TYPE.UNSUB] (msg) { + [NOTIFICATION_TYPE.UNSUB] (msg: Message) { console.debug(`[pubsub] Ignoring ${msg.type} message:`, msg.data) }, - [RESPONSE_TYPE.ERROR] ({ data: { type, contractID } }) { + [RESPONSE_TYPE.ERROR] (this: PubsubClient, { data: { type, contractID } }: Message) { console.warn(`[pubsub] Received ERROR response for ${type} request to ${contractID}`) const client = this @@ -365,7 +541,7 @@ const defaultMessageHandlers = { } }, - [RESPONSE_TYPE.SUCCESS] ({ data: { type, contractID } }) { + [RESPONSE_TYPE.SUCCESS] (this: PubsubClient, { data: { type, contractID } }: Message) { const client = this switch (type) { @@ -411,13 +587,16 @@ const defaultOptions = { const globalEventNames = ['offline', 'online'] const socketEventNames = ['close', 'error', 'message', 'open'] +type SocketEventName = 'close' | 'error' | 'message' | 'open'; + // `navigator.onLine` can give confusing false positives when `true`, // so we'll define `isDefinetelyOffline()` rather than `isOnline()` or `isOffline()`. // See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine +// @ts-ignore TS2339 [ERROR]: Property 'onLine' does not exist on type 'Navigator'. const isDefinetelyOffline = () => typeof navigator === 'object' && navigator.onLine === false // Parses and validates a received message. -export const messageParser = (data) => { +export const messageParser = (data: string): Message => { const msg = JSON.parse(data) if (typeof msg !== 'object' || msg === null) { @@ -431,173 +610,11 @@ export const messageParser = (data) => { return msg } -const publicMethods = { - clearAllTimers () { - const client = this - - clearTimeout(client.connectionTimeoutID) - clearTimeout(client.nextConnectionAttemptDelayID) - clearTimeout(client.pingTimeoutID) - client.connectionTimeoutID = undefined - client.nextConnectionAttemptDelayID = undefined - client.pingTimeoutID = undefined - }, - - // Performs a connection or reconnection attempt. - connect () { - const client = this - - if (client.socket !== null) { - throw new Error('connect() can only be called if there is no current socket.') - } - if (client.nextConnectionAttemptDelayID) { - throw new Error('connect() must not be called during a reconnection delay.') - } - if (!client.shouldReconnect) { - throw new Error('connect() should no longer be called on this instance.') - } - client.socket = new WebSocket(client.url) - - if (client.options.timeout) { - client.connectionTimeoutID = setTimeout(() => { - client.connectionTimeoutID = undefined - client.socket?.close(4000, 'timeout') - }, client.options.timeout) - } - // Attach WebSocket event listeners. - for (const name of socketEventNames) { - client.socket.addEventListener(name, client.listeners[name]) - } - }, - - /** - * Immediately close the socket, stop listening for events and clear any cache. - * - * This method is used in unit tests. - * - In particular, no 'close' event handler will be called. - * - Any incoming or outgoing buffered data will be discarded. - * - Any pending messages will be discarded. - */ - destroy () { - const client = this - - client.clearAllTimers() - // Update property values. - // Note: do not clear 'client.options'. - client.pendingSubscriptionSet.clear() - client.pendingUnsubscriptionSet.clear() - client.subscriptionSet.clear() - // Remove global event listeners. - if (typeof window === 'object') { - for (const name of globalEventNames) { - window.removeEventListener(name, client.listeners[name]) - } - } - // Remove WebSocket event listeners. - if (client.socket) { - for (const name of socketEventNames) { - client.socket.removeEventListener(name, client.listeners[name]) - } - client.socket.close(4001, 'destroy') - } - client.listeners = {} - client.socket = null - client.shouldReconnect = false - }, - - getNextRandomDelay () { - const client = this - - const { - maxReconnectionDelay, - minReconnectionDelay, - reconnectionDelayGrowFactor - } = client.options - - const minDelay = minReconnectionDelay * reconnectionDelayGrowFactor ** client.failedConnectionAttempts - const maxDelay = minDelay * reconnectionDelayGrowFactor - - return Math.min(maxReconnectionDelay, Math.round(minDelay + Math.random() * (maxDelay - minDelay))) - }, - - // Schedules a connection attempt to happen after a delay computed according to - // a randomized exponential backoff algorithm variant. - scheduleConnectionAttempt () { - const client = this - - if (!client.shouldReconnect) { - throw new Error('Cannot call `scheduleConnectionAttempt()` when `shouldReconnect` is false.') - } - if (client.nextConnectionAttemptDelayID) { - return console.warn('[pubsub] A reconnection attempt is already scheduled.') - } - const delay = client.getNextRandomDelay() - const nth = client.failedConnectionAttempts + 1 - - client.nextConnectionAttemptDelayID = setTimeout(() => { - sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_ATTEMPT, client) - client.nextConnectionAttemptDelayID = undefined - client.connect() - }, delay) - sbp('okTurtles.events/emit', PUBSUB_RECONNECTION_SCHEDULED, client, { delay, nth }) - }, - - // Unused for now. - pub (contractID, data, dontBroadcast = false) { - }, - - /** - * Sends a SUB request to the server as soon as possible. - * - * - The given contract ID will be cached until we get a relevant server - * response, allowing us to resend the same request if necessary. - * - Any identical UNSUB request that has not been sent yet will be cancelled. - * - Calling this method again before the server has responded has no effect. - * @param contractID - The ID of the contract whose updates we want to subscribe to. - */ - sub (contractID, dontBroadcast = false) { - const client = this - const { socket } = this - - if (!client.pendingSubscriptionSet.has(contractID)) { - client.pendingSubscriptionSet.add(contractID) - client.pendingUnsubscriptionSet.delete(contractID) - - if (socket?.readyState === WebSocket.OPEN) { - socket.send(createRequest(REQUEST_TYPE.SUB, { contractID }, dontBroadcast)) - } - } - }, - - /** - * Sends an UNSUB request to the server as soon as possible. - * - * - The given contract ID will be cached until we get a relevant server - * response, allowing us to resend the same request if necessary. - * - Any identical SUB request that has not been sent yet will be cancelled. - * - Calling this method again before the server has responded has no effect. - * @param contractID - The ID of the contract whose updates we want to unsubscribe from. - */ - unsub (contractID, dontBroadcast = false) { - const client = this - const { socket } = this - - if (!client.pendingUnsubscriptionSet.has(contractID)) { - client.pendingSubscriptionSet.delete(contractID) - client.pendingUnsubscriptionSet.add(contractID) - - if (socket?.readyState === WebSocket.OPEN) { - socket.send(createRequest(REQUEST_TYPE.UNSUB, { contractID }, dontBroadcast)) - } - } - } -} - // Register custom SBP event listeners before the first connection. for (const name of Object.keys(defaultClientEventHandlers)) { if (name === 'error' || !socketEventNames.includes(name)) { - sbp('okTurtles.events/on', `pubsub-${name}`, (target, detail) => { - target.listeners[name]({ type: name, target, detail }) + sbp('okTurtles.events/on', `pubsub-${name}`, (target: PubsubClient, detail: any) => { + target.listeners[name](({ type: name, target, detail } as unknown) as Event) }) } } diff --git a/shared/types.ts b/shared/types.ts index 891431fb41..a211a80ee7 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -8,9 +8,9 @@ // https://flowtype.org/docs/modules.html#import-type // https://flowtype.org/docs/advanced-configuration.html -export type JSONType = string | number | boolean | null | JSONObject | JSONArray; -export type JSONObject = { [key:string]: JSONType }; -export type JSONArray = Array; +export type JSONType = string | number | boolean | null | JSONObject | JSONArray +export type JSONObject = { [key: string]: JSONType } +export type JSONArray = Array export type ResType = | ResTypeErr | ResTypeOK | ResTypeAlready @@ -27,7 +27,7 @@ export type ResTypeEntry = 'entry' // https://github.com/facebook/flow/issues/3041 export type Response = { // export interface Response { - type: ResType; - err?: string; + type: ResType + err?: string data?: JSONType } From 4800658ac813042c6b0b186fb0fa0b7cb140ce30 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Tue, 20 Sep 2022 20:19:52 +0200 Subject: [PATCH 22/43] Adding typescript linting --- Gruntfile.js | 2 +- backend/.eslintrc.json | 9 +- backend/auth.ts | 2 +- backend/database.ts | 10 +- backend/index.ts | 11 +- backend/pubsub.ts | 141 +++---- backend/routes.ts | 12 +- backend/types.ts | 43 +- frontend/.eslintrc.json | 32 -- frontend/controller/namespace.js | 2 +- package.json | 1 + shared/.eslintrc.json | 23 ++ shared/domains/chelonia/GIMessage.ts | 54 +-- shared/domains/chelonia/chelonia.ts | 205 ++++++---- shared/domains/chelonia/db.ts | 16 +- shared/domains/chelonia/errors.ts | 1 + shared/domains/chelonia/internals.ts | 72 +++- shared/functions.ts | 17 +- shared/pubsub.test.ts | 13 +- shared/pubsub.ts | 112 +++-- test/backend.test.js | 386 ------------------ test/backend.test.ts | 3 +- test/contracts/chatroom.js | 9 +- test/contracts/chatroom.js.map | 4 +- test/contracts/group.js | 10 +- test/contracts/group.js.map | 4 +- test/contracts/identity.js | 9 +- test/contracts/identity.js.map | 4 +- test/contracts/mailbox.js | 9 +- test/contracts/mailbox.js.map | 4 +- test/contracts/shared/payments/index.js.map | 4 +- test/contracts/shared/voting/proposals.js | 9 +- test/contracts/shared/voting/proposals.js.map | 4 +- 33 files changed, 493 insertions(+), 744 deletions(-) delete mode 100644 frontend/.eslintrc.json create mode 100644 shared/.eslintrc.json delete mode 100644 test/backend.test.js diff --git a/Gruntfile.js b/Gruntfile.js index 4860da3e06..08c381fcf5 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -397,7 +397,7 @@ module.exports = (grunt) => { options: { env: process.env } }, // Test anything in /test that ends with `.test.ts`. - testWithDeno: 'deno test --allow-env --allow-net --allow-read --allow-write --importmap=import-map.json --no-check ./test/*.ts', + testWithDeno: 'deno test --allow-env --allow-net --allow-read --allow-write --import-map=import-map.json --no-check ./test/*.ts', chelDeployAll: 'find contracts -iname "*.manifest.json" | xargs -r ./node_modules/.bin/chel deploy ./data' } }) diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json index 73ae078d61..bb4118c811 100644 --- a/backend/.eslintrc.json +++ b/backend/.eslintrc.json @@ -8,13 +8,16 @@ "import" ], "rules": { + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-this-alias": "off", "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], - "require-await": "error", - "quote-props": "off", "dot-notation": "off", "import/extensions": [ 2, "ignorePackages" - ] + ], + "no-use-before-define": "off", + "quote-props": "off", + "require-await": "error" } } diff --git a/backend/auth.ts b/backend/auth.ts index 2fff7f6ca5..78a94df3e6 100644 --- a/backend/auth.ts +++ b/backend/auth.ts @@ -9,7 +9,7 @@ const { PermissionDenied } = Deno.errors export default { name: 'gi-auth', - register (server: Object, opts: Object) { + register (server: unknown, opts: unknown) { server.auth.scheme('gi-auth', function (server, options) { return { authenticate (request, h) { diff --git a/backend/database.ts b/backend/database.ts index c2342f68dc..da6af19ea4 100644 --- a/backend/database.ts +++ b/backend/database.ts @@ -57,7 +57,7 @@ export default sbp('sbp/selectors/register', { chunks.push(']') break } else { - currentHEAD = entry.message().previousHEAD + currentHEAD = entry.message().previousHEAD } } } catch (error) { @@ -67,7 +67,7 @@ export default sbp('sbp/selectors/register', { }, 'backend/db/streamEntriesBefore': async function (before: string, limit: number): Promise { let currentHEAD = before - let entry = await sbp('chelonia/db/getEntry', currentHEAD) + const entry = await sbp('chelonia/db/getEntry', currentHEAD) if (!entry) { throw new NotFound(`entry ${currentHEAD} doesn't exist!`) } @@ -100,7 +100,7 @@ export default sbp('sbp/selectors/register', { 'backend/db/streamEntriesBetween': async function (startHash: string, endHash: string, offset: number): Promise { let isMet = false let currentHEAD = endHash - let entry = await sbp('chelonia/db/getEntry', currentHEAD) + const entry = await sbp('chelonia/db/getEntry', currentHEAD) if (!entry) { throw new NotFound(`entry ${currentHEAD} doesn't exist!`) } @@ -172,14 +172,14 @@ export default sbp('sbp/selectors/register', { } return await readFileAsync(filepath) }, - 'backend/db/writeFile': async function (filename: string, data: any) { + 'backend/db/writeFile': async function (filename: string, data: Uint8Array) { // TODO: check for how much space we have, and have a server setting // that determines how much of the disk space we're allowed to // use. If the size of the file would cause us to exceed this // amount, throw an exception return await writeFileAsync(throwIfFileOutsideDataDir(filename), data) }, - 'backend/db/writeFileOnce': async function (filename: string, data: any) { + 'backend/db/writeFileOnce': async function (filename: string, data: Uint8Array) { const filepath = throwIfFileOutsideDataDir(filename) if (await fileExists(filepath)) { console.warn('writeFileOnce: exists:', filepath) diff --git a/backend/index.ts b/backend/index.ts index 9b811baa03..fa0b4e71d8 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -1,8 +1,6 @@ -declare var process: any - import { bold } from 'fmt/colors.ts' -import sbp from '@sbp/sbp' +import sbp from '@sbp/sbp' import '@sbp/okturtles.data' import '@sbp/okturtles.events' import { notFound } from 'pogo/lib/bang.ts' @@ -12,7 +10,7 @@ import { SERVER_RUNNING } from './events.ts' import { PUBSUB_INSTANCE } from './instance-keys.ts' import type { PubsubClient, PubsubServer } from './pubsub.ts' -// @ts-ignore +// @ts-expect-error TS7017 [ERROR]: Element implicitly has an 'any' type. globalThis.logger = function (err: Error) { console.error(err) err.stack && console.error(err.stack) @@ -25,7 +23,7 @@ globalThis.logger = function (err: Error) { const dontLog: Record = { 'backend/server/broadcastEntry': true } -function logSBP (domain: string, selector: string, data: any) { +function logSBP (domain: string, selector: string, data: unknown) { if (!(selector in dontLog)) { console.log(bold(`[sbp] ${selector}`), data) } @@ -67,7 +65,8 @@ const shutdownFn = function () { }) } -Deno.addSignalListener('SIGUSR2', shutdownFn) +Deno.addSignalListener('SIGBREAK', shutdownFn) +Deno.addSignalListener('SIGINT', shutdownFn) // Equivalent to the `uncaughtException` event in Nodejs. addEventListener('error', (event) => { diff --git a/backend/pubsub.ts b/backend/pubsub.ts index 8a207cd21a..2fa2ae4336 100644 --- a/backend/pubsub.ts +++ b/backend/pubsub.ts @@ -1,33 +1,30 @@ +import { messageParser } from '~/shared/pubsub.ts' + +/* eslint-disable @typescript-eslint/no-explicit-any */ +type Callback = (...args: any[]) => void + type JSONType = ReturnType -type Message = { - [key: string]: JSONType, +interface Message { + [key: string]: JSONType type: string } -type SubMessage = { - contractID: string - dontBroadcast?: boolean -} - -type UnsubMessage = { - contractID: string - dontBroadcast?: boolean -} +type MessageHandler = (this: PubsubServer, msg: Message) => void type PubsubClientEventName = 'close' | 'message' type PubsubServerEventName = 'close' | 'connection' | 'error' | 'headers' | 'listening' -import { - acceptWebSocket, - isWebSocketCloseEvent, - isWebSocketPingEvent, -} from 'https://deno.land/std@0.92.0/ws/mod.ts' - -import { messageParser } from '~/shared/pubsub.ts' - -const CI = Deno.env.get('CI') -const NODE_ENV = Deno.env.get('NODE_ENV') ?? 'development' +type PubsubServerOptions = { + clientHandlers?: Record + logPingRounds?: boolean + logPongMessages?: boolean + maxPayload?: number + messageHandlers?: Record + pingInterval?: number + rawHttpServer?: unknown + serverHandlers?: Record +} const emptySet = Object.freeze(new Set()) // Used to tag console output. @@ -35,8 +32,6 @@ const tag = '[pubsub]' // ====== Helpers ====== // -// Only necessary when using the `ws` module in Deno. - const generateSocketID = (() => { let counter = 0 @@ -46,12 +41,14 @@ const generateSocketID = (() => { const logger = { log: console.log.bind(console, tag), debug: console.debug.bind(console, tag), - error: console.error.bind(console, tag) + error: console.error.bind(console, tag), + info: console.info.bind(console, tag), + warn: console.warn.bind(console, tag) } // ====== API ====== // -export function createErrorResponse (data: Object): string { +export function createErrorResponse (data: JSONType): string { return JSON.stringify({ type: 'error', data }) } @@ -59,17 +56,17 @@ export function createMessage (type: string, data: JSONType): string { return JSON.stringify({ type, data }) } -export function createNotification (type: string, data: Object): string { +export function createNotification (type: string, data: JSONType): string { return JSON.stringify({ type, data }) } -export function createResponse (type: string, data: Object): string { +export function createResponse (type: string, data: JSONType): string { return JSON.stringify({ type, data }) } export class PubsubClient { - id: string activeSinceLastPing: boolean + id: string pinged: boolean server: PubsubServer socket: WebSocket @@ -94,7 +91,7 @@ export class PubsubClient { this.terminate() } - send (data: string | ArrayBufferLike | Blob | ArrayBufferView): void { + send (data: string | ArrayBufferLike | ArrayBufferView | Blob): void { const { socket } = this if (socket.readyState === WebSocket.OPEN) { this.socket.send(data) @@ -105,12 +102,12 @@ export class PubsubClient { terminate () { const { server, socket } = this - internalClientEventHandlers.close.call(this, 1000, '') + internalClientEventHandlers.close.call(this, new CloseEvent('close', { code: 4001, reason: 'terminated' })) // Remove listeners for socket events, i.e. events emitted on a socket object. ;['close', 'error', 'message', 'ping', 'pong'].forEach((eventName: string) => { socket.removeEventListener(eventName, internalClientEventHandlers[eventName as PubsubClientEventName] as EventListener) - if (typeof server.customClientEventHandlers[eventName] === 'function') { - socket.removeEventListener(eventName as keyof WebSocketEventMap, server.customClientEventHandlers[eventName] as EventListener) + if (typeof server.customClientHandlers[eventName] === 'function') { + socket.removeEventListener(eventName as keyof WebSocketEventMap, server.customClientHandlers[eventName] as EventListener) } }) socket.close() @@ -119,19 +116,19 @@ export class PubsubClient { export class PubsubServer { clients: Set - customServerEventHandlers: Record - customClientEventHandlers: Record - messageHandlers: Record - options: any + customServerHandlers: Record + customClientHandlers: Record + messageHandlers: Record void> + options: typeof defaultOptions pingIntervalID?: number - queuesByEventName: Map> + queuesByEventName: Map> subscribersByContractID: Record> - constructor (options: Object = {}) { + constructor (options: PubsubServerOptions = {}) { this.clients = new Set() - this.customServerEventHandlers = Object.create(null) - this.customClientEventHandlers = Object.create(null) - this.messageHandlers = { ...defaultMessageHandlers, ...(options as any).customMessageHandlers } + this.customClientHandlers = options.clientHandlers ?? Object.create(null) + this.customServerHandlers = options.serverHandlers ?? Object.create(null) + this.messageHandlers = { ...defaultMessageHandlers, ...options.messageHandlers } this.options = { ...defaultOptions, ...options } this.queuesByEventName = new Map() this.subscribersByContractID = Object.create(null) @@ -151,12 +148,12 @@ export class PubsubServer { return response } - emit (name: string, ...args: any[]) { + emit (name: string, ...args: unknown[]) { const server = this const queue = server.queuesByEventName.get(name) ?? emptySet try { for (const callback of queue) { - Function.prototype.call.call(callback as Function, server, ...args) + Function.prototype.call.call(callback as Callback, server, ...args) } } catch (error) { if (server.queuesByEventName.has('error')) { @@ -167,13 +164,13 @@ export class PubsubServer { } } - off (name: string, callback: Function) { + off (name: string, callback: Callback) { const server = this const queue = server.queuesByEventName.get(name) ?? emptySet queue.delete(callback) } - on (name: string, callback: Function) { + on (name: string, callback: Callback) { const server = this if (!server.queuesByEventName.has(name)) { server.queuesByEventName.set(name, new Set()) @@ -182,10 +179,6 @@ export class PubsubServer { queue?.add(callback) } - get port () { - return this.options.rawHttpServer?.listener?.addr?.port - } - /** * Broadcasts a message, ignoring clients which are not open. * @@ -214,17 +207,18 @@ export class PubsubServer { } } -export function createServer(options = {}) { +export function createServer (options: PubsubServerOptions = {}) { const server = new PubsubServer(options) // Add listeners for server events, i.e. events emitted on the server object. - Object.keys(internalServerHandlers).forEach((name: string) => { - server.on(name, (...args: any[]) => { + Object.keys(internalServerEventHandlers).forEach((name: string) => { + server.on(name, (...args: unknown[]) => { try { // Always call the default handler first. - // @ts-ignore TS2556 [ERROR]: A spread argument must either have a tuple type or be passed to a rest parameter. - internalServerHandlers[name as PubsubServerEventName]?.call(server, ...args) - server.customServerEventHandlers[name as PubsubServerEventName]?.call(server, ...args) + // @ts-expect-error TS2556 [ERROR]: A spread argument must either have a tuple type or be passed to a rest parameter. + internalServerEventHandlers[name as PubsubServerEventName]?.call(server, ...args) + // @ts-expect-error TS2556 [ERROR]: A spread argument must either have a tuple type or be passed to a rest parameter. + server.customServerHandlers[name as PubsubServerEventName]?.call(server, ...args) } catch (error) { server.emit('error', error) } @@ -255,20 +249,13 @@ export function createServer(options = {}) { export function isUpgradeableRequest (request: Request): boolean { const upgrade = request.headers.get('upgrade') - if (upgrade?.toLowerCase() === "websocket") return true + if (upgrade?.toLowerCase() === 'websocket') return true return false } -const defaultOptions = { - logPingRounds: true, - logPongMessages: true, - maxPayload: 6 * 1024 * 1024, - pingInterval: 30000 -} - // Internal default handlers for server events. // The `this` binding refers to the server object. -const internalServerHandlers = Object.freeze({ +const internalServerEventHandlers = { close () { logger.log('Server closed') }, @@ -285,7 +272,7 @@ const internalServerHandlers = Object.freeze({ const url = request.url const urlSearch = url.includes('?') ? url.slice(url.lastIndexOf('?')) : '' const debugID = new URLSearchParams(urlSearch).get('debugID') || '' - + const client = new PubsubClient(socket, server) client.id = generateSocketID(debugID) client.activeSinceLastPing = true @@ -301,9 +288,9 @@ const internalServerHandlers = Object.freeze({ logger.log(`Event '${eventName}' on client ${client.id}`, ...args.map(arg => String(arg))) } try { - // @ts-ignore TS2554 [ERROR]: Expected 3 arguments, but got 2. + // @ts-expect-error TS2554 [ERROR]: Expected 3 arguments, but got 2. (internalClientEventHandlers)[eventName as PubsubClientEventName]?.call(client, ...args) - server.customClientEventHandlers[eventName]?.call(client, ...args) + server.customClientHandlers[eventName]?.call(client, ...args) } catch (error) { server.emit('error', error) client.terminate() @@ -319,14 +306,14 @@ const internalServerHandlers = Object.freeze({ listening () { logger.log('Server listening') } -}) +} // Default handlers for server-side client socket events. // The `this` binding refers to the connected `ws` socket object. const internalClientEventHandlers = Object.freeze({ - close (this: PubsubClient, code: number, reason: string) { + close (this: PubsubClient, event: CloseEvent) { const client = this - const { server, socket, id: socketID } = this + const { server, id: socketID } = this // Notify other clients that this one has left any room they shared. for (const contractID of client.subscriptions) { @@ -343,8 +330,8 @@ const internalClientEventHandlers = Object.freeze({ message (this: PubsubClient, event: MessageEvent) { const client = this - const { server, socket } = this - const { type, data } = event + const { server } = this + const { data } = event const text = data let msg: Message = { type: '' } @@ -403,7 +390,7 @@ const defaultMessageHandlers = { // Currently unused. }, - [SUB] (this: PubsubClient, { contractID, dontBroadcast }: SubMessage) { + [SUB] (this: PubsubClient, { contractID, dontBroadcast }: Message) { const client = this const { server, socket, id: socketID } = this @@ -427,7 +414,7 @@ const defaultMessageHandlers = { socket.send(createResponse(SUCCESS, { type: SUB, contractID })) }, - [UNSUB] (this: PubsubClient, { contractID, dontBroadcast }: UnsubMessage) { + [UNSUB] (this: PubsubClient, { contractID, dontBroadcast }: Message) { const client = this const { server, socket, id: socketID } = this @@ -451,3 +438,9 @@ const defaultMessageHandlers = { } } +const defaultOptions = { + logPingRounds: true, + logPongMessages: true, + maxPayload: 6 * 1024 * 1024, + pingInterval: 30000 +} diff --git a/backend/routes.ts b/backend/routes.ts index 2f7c0fedef..148e1d7cb6 100644 --- a/backend/routes.ts +++ b/backend/routes.ts @@ -1,5 +1,3 @@ -/* globals Deno */ - import { bold, yellow } from 'fmt/colors.ts' import sbp from '@sbp/sbp' @@ -15,13 +13,15 @@ import Toolkit from 'pogo/lib/toolkit.ts' import './database.ts' import * as pathlib from 'path' -declare const logger: Function +declare const logger: (err: Error) => Error export const router = new Router() -const route: Record = new Proxy({}, { - get: function (obj: any, prop: string) { - return function (path: string, handler: RouteHandler) { +type RouterProxyTarget = Record Response> + +const route = new Proxy({} as RouterProxyTarget, { + get (obj: RouterProxyTarget, prop: string) { + return (path: string, handler: RouteHandler) => { router.add({ path, method: prop, handler }) } } diff --git a/backend/types.ts b/backend/types.ts index c1c52e10bb..b23cf32c39 100644 --- a/backend/types.ts +++ b/backend/types.ts @@ -2,35 +2,36 @@ import Request from './request.ts' import ServerResponse from './response.ts' import Toolkit from './toolkit.ts' -export interface Route { - method: string, - path: string, - handler: RouteHandler, - vhost?: string -} - -export type RequestParams = { [param: string]: string } -export type RequestState = { [name: string]: string } +type JSONStringifyable = boolean | null | number | object | string -export interface RouteOptions extends Omit, 'method' | 'path'> { - method?: Route['method'] | Iterable, - path?: Route['path'] | Iterable +export interface MatchedRoute extends NormalizedRoute { + params: RequestParams } export interface NormalizedRoute extends Route { - paramNames: Array, - segments: Array + paramNames: Array, + segments: Array } -export interface MatchedRoute extends NormalizedRoute { - params: RequestParams -} +export type RequestParams = { [param: string]: string } +export type RequestState = { [name: string]: string } -type JSONStringifyable = boolean | null | number | object | string export type ResponseBody = Deno.Reader | Uint8Array | JSONStringifyable -export type RouteHandlerResult = ServerResponse | ResponseBody | Error | Promise + +export interface Route { + method: string, + path: string, + handler: RouteHandler, + vhost?: string +} export type RouteHandler = (request: Request, h: Toolkit) => RouteHandlerResult +export type RouteHandlerResult = ServerResponse | ResponseBody | Error | Promise + +export interface RouteOptions extends Omit, 'method' | 'path'> { + method?: Route['method'] | Iterable, + path?: Route['path'] | Iterable +} -export type { ServerOptions } from './server.ts' -export type { FileHandlerOptions } from './helpers/file.ts' export type { DirectoryHandlerOptions } from './helpers/directory.tsx' +export type { FileHandlerOptions } from './helpers/file.ts' +export type { ServerOptions } from './server.ts' diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json deleted file mode 100644 index 08d6073831..0000000000 --- a/frontend/.eslintrc.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "parserOptions": { - "parser": "@babel/eslint-parser" - }, - "extends": [ - "plugin:cypress/recommended", - "plugin:flowtype/recommended", - "plugin:vue/essential", - "standard" - ], - "plugins": [ - "cypress", - "flowtype", - "import" - ], - "ignorePatterns": [ - "assets/*", - "model/contracts/misc/flowTyper.js" - ], - "rules": { - "require-await": "error", - "vue/max-attributes-per-line": "off", - "vue/html-indent": "off", - "flowtype/no-types-missing-file-annotation": "off", - "quote-props": "off", - "dot-notation": "off", - "import/extensions": [ - 2, - "ignorePackages" - ] - } -} diff --git a/frontend/controller/namespace.js b/frontend/controller/namespace.js index 92ce12e4e8..a6bd1b5d67 100644 --- a/frontend/controller/namespace.js +++ b/frontend/controller/namespace.js @@ -26,7 +26,7 @@ sbp('sbp/selectors/register', { return cache[name] } return fetch(`${sbp('okTurtles.data/get', 'API_URL')}/name/${name}`).then((r) => { - if (!r.ok) { + if (!r.ok) { console.warn(`namespace/lookup: ${r.status} for ${name}`) if (r.status !== 404) { throw new Error(`${r.status}: ${r.statusText}`) diff --git a/package.json b/package.json index dba4a138aa..7e9d368163 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "eslintIgnore": [ "frontend/assets/*", "frontend/model/contracts/misc/flowTyper.js", + "shared/declarations.js", "historical/*", "shared/types.js", "dist/*", diff --git a/shared/.eslintrc.json b/shared/.eslintrc.json new file mode 100644 index 0000000000..5624761085 --- /dev/null +++ b/shared/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "parserOptions": { + "parser": "@typescript-eslint/parser" + }, + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "plugins": [ + "@typescript-eslint", + "import" + ], + "rules": { + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "dot-notation": "off", + "import/extensions": [ + 2, + "ignorePackages" + ], + "no-use-before-define": "off", + "quote-props": "off", + "require-await": "error" + } +} diff --git a/shared/domains/chelonia/GIMessage.ts b/shared/domains/chelonia/GIMessage.ts index 6180cbab18..940c67c29f 100644 --- a/shared/domains/chelonia/GIMessage.ts +++ b/shared/domains/chelonia/GIMessage.ts @@ -6,27 +6,35 @@ import type { JSONObject } from '~/shared/types.ts' type JSONType = ReturnType type Mapping = { - key: string; - value: string; + key: string + value: string } type Message = { - version: string; // Semver version string - previousHEAD: string | null; - contractID: string | null; - op: GIOp - manifest: string; + contractID: string | null + manifest: string // The nonce makes it difficult to predict message contents // and makes it easier to prevent conflicts during development. - nonce: number; + nonce: number + op: GIOp + previousHEAD: string | null + version: string // Semver version string } +type Signature = { + sig: string + type: string +} + +type DecryptFunction = (v: GIOpActionEncrypted) => GIOpActionUnencrypted +type SignatureFunction = (data: string) => Signature + export type GIKeyType = '' export type GIKey = { - type: GIKeyType; - data: Object; // based on GIKeyType this will change - meta: Object; + type: GIKeyType + data: JSONType // based on GIKeyType this will change + meta: JSONObject } // Allows server to check if the user is allowed to register this type of contract // TODO: rename 'type' to 'contractName': @@ -45,14 +53,14 @@ export class GIMessage { _mapping: Mapping _message: Message - static OP_CONTRACT: 'c' = 'c' - static OP_ACTION_ENCRYPTED: 'ae' = 'ae' // e2e-encrypted action - static OP_ACTION_UNENCRYPTED: 'au' = 'au' // publicly readable action - static OP_KEY_ADD: 'ka' = 'ka' // add this key to the list of keys allowed to write to this contract, or update an existing key - static OP_KEY_DEL: 'kd' = 'kd' // remove this key from authorized keys - static OP_PROTOCOL_UPGRADE: 'pu' = 'pu' - static OP_PROP_SET: 'ps' = 'ps' // set a public key/value pair - static OP_PROP_DEL: 'pd' = 'pd' // delete a public key/value pair + static OP_CONTRACT = 'c' as const + static OP_ACTION_ENCRYPTED = 'ae' as const // e2e-encrypted action + static OP_ACTION_UNENCRYPTED = 'au' as const // publicly readable action + static OP_KEY_ADD = 'ka' as const // add this key to the list of keys allowed to write to this contract, or update an existing key + static OP_KEY_DEL = 'kd' as const // remove this key from authorized keys + static OP_PROTOCOL_UPGRADE = 'pu' as const + static OP_PROP_SET = 'ps' as const // set a public key/value pair + static OP_PROP_DEL = 'pd' as const // delete a public key/value pair // eslint-disable-next-line camelcase static createV1_0 ( @@ -60,7 +68,7 @@ export class GIMessage { previousHEAD: string | null = null, op: GIOp, manifest: string, - signatureFn: Function = defaultSignatureFn + signatureFn: SignatureFunction = defaultSignatureFn ): GIMessage { const message: Message = { version: '1.0.0', @@ -113,11 +121,11 @@ export class GIMessage { } } - decryptedValue (fn?: Function): any { + decryptedValue (fn?: DecryptFunction): GIOpValue { if (!this._decrypted) { this._decrypted = ( this.opType() === GIMessage.OP_ACTION_ENCRYPTED && fn !== undefined - ? fn(this.opValue()) + ? fn(this.opValue() as string) : this.opValue() ) } @@ -160,7 +168,7 @@ export class GIMessage { hash (): string { return this._mapping.key } } -function defaultSignatureFn (data: string) { +function defaultSignatureFn (data: string): Signature { return { type: 'default', sig: blake32Hash(data) diff --git a/shared/domains/chelonia/chelonia.ts b/shared/domains/chelonia/chelonia.ts index 33e56b35fc..924f420e6d 100644 --- a/shared/domains/chelonia/chelonia.ts +++ b/shared/domains/chelonia/chelonia.ts @@ -1,5 +1,4 @@ -declare var process: any - +/* eslint-disable camelcase */ import sbp from '@sbp/sbp' import '@sbp/okturtles.events' import '@sbp/okturtles.eventqueue' @@ -12,10 +11,38 @@ import { handleFetchResult } from '~/frontend/controller/utils/misc.js' // TODO: rename this to ChelMessage import { GIMessage } from './GIMessage.ts' import { ChelErrorUnrecoverable } from './errors.ts' -import type { GIOpContract, GIOpActionUnencrypted } from './GIMessage.ts' +import type { GIOpActionUnencrypted } from './GIMessage.ts' +declare const process: { + env: Record +} type JSONType = ReturnType +export type ChelActionParams = { + action: string; + server?: string; // TODO: implement! + contractID: string; + data: JSONType; + hooks?: { + prepublishContract?: (msg: GIMessage) => void; + prepublish?: (msg: GIMessage) => void; + postpublish?: (msg: GIMessage) => void; + }; + publishOptions?: { maxAttempts: number }; +} + +export type ChelRegParams = { + contractName: string; + server?: string; // TODO: implement! + data: JSONType; + hooks?: { + prepublishContract?: (msg: GIMessage) => void; + prepublish?: (msg: GIMessage) => void; + postpublish?: (msg: GIMessage) => void; + }; + publishOptions?: { maxAttempts: number }; +} + export type CheloniaConfig = { connectionOptions: { maxRetries: number @@ -25,14 +52,14 @@ export type CheloniaConfig = { connectionURL: null | string contracts: { defaults: { - modules: Record + modules: Record exposedGlobals: Record allowedDomains: string[] allowedSelectors: string[] preferSlim: boolean } manifests: Record // Contract names -> manifest hashes - overrides: Record // Override default values per-contract. + overrides: Record // Override default values per-contract. } decryptFn: (arg: string) => JSONType encryptFn: (arg: JSONType) => string @@ -45,10 +72,26 @@ export type CheloniaConfig = { syncContractError: null | ((e: Error, contractID: string) => void) pubsubError: null | ((e: Error, socket: WebSocket) => void) } - postOp?: (state: any, message: any) => boolean - preOp?: (state: any, message: any) => boolean - reactiveDel: (obj: any, key: string) => void - reactiveSet: (obj: any, key: string, value: any) => typeof value + postOp?: (state: unknown, message: unknown) => boolean + postOp_ae?: PostOp + postOp_au?: PostOp + postOp_c?: PostOp + postOp_ka?: PostOp + postOp_kd?: PostOp + postOp_pd?: PostOp + postOp_ps?: PostOp + postOp_pu?: PostOp + preOp?: PreOp + preOp_ae?: PreOp + preOp_au?: PreOp + preOp_c?: PreOp + preOp_ka?: PreOp + preOp_kd?: PreOp + preOp_pd?: PreOp + preOp_ps?: PreOp + preOp_pu?: PreOp + reactiveDel: (obj: Record, key: string) => void + reactiveSet: (obj: Record, key: string, value: unknown) => typeof value skipActionProcessing: boolean skipSideEffects: boolean skipProcessing?: boolean @@ -59,68 +102,86 @@ export type CheloniaConfig = { export type CheloniaInstance = { config: CheloniaConfig; contractsModifiedListener?: () => void; - contractSBP?: any; - defContract?: ContractDefinition; + contractSBP?: unknown; + defContract: ContractDefinition; defContractManifest?: string; defContractSBP?: SBP; defContractSelectors?: string[]; - manifestToContract: Record; - sideEffectStack: (contractID: string) => any[]; - sideEffectStacks: Record; + manifestToContract: Record + sideEffectStack: (contractID: string) => SBPCallParameters[]; + sideEffectStacks: Record; state: CheloniaState; - whitelistedActions: Record; + whitelistedActions: Record; } -export type CheloniaState = { - contracts: Record; // Contracts we've subscribed to. - pending: string[]; // Prevents processing unexpected data from a malicious server. +export interface CheloniaState { + [contractID: string]: unknown + contracts: Record // contractIDs => { type:string, HEAD:string } (for contracts we've successfully subscribed to) + pending: string[] // Prevents processing unexpected data from a malicious server. } +type Action = { + validate: (data: JSONType, { state, getters, meta, contractID }: { + state: CheloniaState + getters: Getters + meta: JSONType + contractID: string + }) => boolean | void + process: (message: Mutation, { state, getters }: { + state: CheloniaState + getters: Getters + }) => void + sideEffect?: (message: Mutation, { state, getters }: { + state: CheloniaState + getters: Getters + }) => void +} + +export type Mutation = { + data: JSONType + meta: JSONType + hash: string + contractID: string +} + +type PostOp = (state: unknown, message: unknown) => boolean +type PreOp = (state: unknown, message: unknown) => boolean + +type SBP = (selector: string, ...args: unknown[]) => unknown + +type SBPCallParameters = [string, ...unknown[]] + export type ContractDefinition = { - actions: any - getters: any + actions: Record + getters: Getters manifest: string metadata: { - create(): any - validate(meta: any, args: any): void + create(): JSONType + validate(meta: JSONType, args: { + state: CheloniaState + getters: Getters + contractID: string + }): void validate(): void } - methods: any + methods: Record unknown> name: string sbp: SBP - state (contractID: string): any + state (contractID: string): CheloniaState // Contract instance state } -type SBP = (selector: string, ...args: any[]) => any - -export type ChelRegParams = { - contractName: string; - server?: string; // TODO: implement! - data: Object; - hooks?: { - prepublishContract?: (msg: GIMessage) => void; - prepublish?: (msg: GIMessage) => void; - postpublish?: (msg: GIMessage) => void; - }; - publishOptions?: { maxAttempts: number }; -} - -export type ChelActionParams = { - action: string; - server?: string; // TODO: implement! - contractID: string; - data: Object; - hooks?: { - prepublishContract?: (msg: GIMessage) => void; - prepublish?: (msg: GIMessage) => void; - postpublish?: (msg: GIMessage) => void; - }; - publishOptions?: { maxAttempts: number }; +export type ContractInfo = { + file: string + hash: string } export { GIMessage } -export const ACTION_REGEX: RegExp = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ +export const ACTION_REGEX = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ // ACTION_REGEX.exec('gi.contracts/group/payment/process') // 0 => 'gi.contracts/group/payment/process' // 1 => 'gi.contracts/group/payment/' @@ -151,8 +212,8 @@ export default sbp('sbp/selectors/register', { manifests: {} // override! contract names => manifest hashes }, whitelisted: (action: string): boolean => !!this.whitelistedActions[action], - reactiveSet: (obj: any, key: string, value: any): typeof value => { obj[key] = value; return value }, // example: set to Vue.set - reactiveDel: (obj: any, key: string): void => { delete obj[key] }, + reactiveSet: (obj: CheloniaState, key: string, value: unknown): typeof value => { obj[key] = value; return value }, // example: set to Vue.set + reactiveDel: (obj: CheloniaState, key: string): void => { delete obj[key] }, skipActionProcessing: false, skipSideEffects: false, connectionOptions: { @@ -176,8 +237,8 @@ export default sbp('sbp/selectors/register', { } this.manifestToContract = {} this.whitelistedActions = {} - this.sideEffectStacks = {} // [contractID]: Array - this.sideEffectStack = (contractID: string): Array => { + this.sideEffectStacks = {} // [contractID]: Array + this.sideEffectStack = (contractID: string): Array => { let stack = this.sideEffectStacks[contractID] if (!stack) { this.sideEffectStacks[contractID] = stack = [] @@ -245,7 +306,7 @@ export default sbp('sbp/selectors/register', { if (!ACTION_REGEX.exec(contract.name)) throw new Error(`bad contract name: ${contract.name}`) if (!contract.metadata) contract.metadata = { validate () {}, create: () => ({}) } if (!contract.getters) contract.getters = {} - contract.state = (contractID) => sbp(this.config.stateSelector)[contractID] + contract.state = (contractID: string) => sbp(this.config.stateSelector)[contractID] contract.manifest = this.defContractManifest contract.sbp = this.defContractSBP this.defContractSelectors = [] @@ -255,7 +316,7 @@ export default sbp('sbp/selectors/register', { [`${contract.manifest}/${contract.name}/getters`]: () => contract.getters, // 2 ways to cause sideEffects to happen: by defining a sideEffect function in the // contract, or by calling /pushSideEffect w/async SBP call. Can also do both. - [`${contract.manifest}/${contract.name}/pushSideEffect`]: (contractID: string, asyncSbpCall: any[]) => { + [`${contract.manifest}/${contract.name}/pushSideEffect`]: (contractID: string, asyncSbpCall: SBPCallParameters) => { // if this version of the contract is pushing a sideEffect to a function defined by the // contract itself, make sure that it calls the same version of the sideEffect const [sel] = asyncSbpCall @@ -275,7 +336,7 @@ export default sbp('sbp/selectors/register', { // - whatever keys should be passed in as well // base it off of the design of encryptedAction() this.defContractSelectors.push(...sbp('sbp/selectors/register', { - [`${contract.manifest}/${action}/process`]: (message: any, state: any) => { + [`${contract.manifest}/${action}/process`]: (message: Mutation, state: CheloniaState) => { const { meta, data, contractID } = message // TODO: optimize so that you're creating a proxy object only when needed const gProxy = gettersProxy(state, contract.getters) @@ -284,7 +345,7 @@ export default sbp('sbp/selectors/register', { contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) contract.actions[action].process(message, { state, ...gProxy }) }, - [`${contract.manifest}/${action}/sideEffect`]: async (message: any, state: any) => { + [`${contract.manifest}/${action}/sideEffect`]: async (message: Mutation, state: CheloniaState) => { const sideEffects = this.sideEffectStack(message.contractID) while (sideEffects.length > 0) { const sideEffect = sideEffects.shift() @@ -292,15 +353,17 @@ export default sbp('sbp/selectors/register', { const [selector, ...args] = sideEffect await contract.sbp(selector, ...args) } catch (e) { + // @ts-expect-error: TS2339 Property 'description' does not exist on type 'Mutation'. console.error(`[chelonia] ERROR: '${e.name}' ${e.message}, for pushed sideEffect of ${message.description()}:`, sideEffect) this.sideEffectStacks[message.contractID] = [] // clear the side effects throw e } } - if (contract.actions[action].sideEffect) { + const { sideEffect } = contract.actions[action] + if (sideEffect) { state = state || contract.state(message.contractID) const gProxy = gettersProxy(state, contract.getters) - await contract.actions[action].sideEffect(message, { state, ...gProxy }) + await sideEffect(message, { state, ...gProxy }) } } })) @@ -338,7 +401,7 @@ export default sbp('sbp/selectors/register', { } }, // resolves when all pending actions for these contractID(s) finish - 'chelonia/contract/wait': function (contractIDs?: string | string[]): Promise { + 'chelonia/contract/wait': function (contractIDs?: string | string[]): Promise { const listOfIds = contractIDs ? (typeof contractIDs === 'string' ? [contractIDs] : contractIDs) : Object.keys(sbp(this.config.stateSelector).contracts) @@ -348,7 +411,7 @@ export default sbp('sbp/selectors/register', { }, // 'chelonia/contract' - selectors related to injecting remote data and monitoring contracts // TODO: add an optional parameter to "retain" the contract (see #828) - 'chelonia/contract/sync': function (contractIDs: string | string[]): Promise { + 'chelonia/contract/sync': function (contractIDs: string | string[]): Promise { const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs return Promise.all(listOfIds.map(contractID => { // enqueue this invocation in a serial queue to ensure @@ -358,7 +421,7 @@ export default sbp('sbp/selectors/register', { // This prevents handleEvent getting called with the wrong previousHEAD for an event. return sbp('chelonia/queueInvocation', contractID, [ 'chelonia/private/in/syncContract', contractID - ]).catch((err: any) => { + ]).catch((err: unknown) => { console.error(`[chelonia] failed to sync ${contractID}:`, err) throw err // re-throw the error }) @@ -366,7 +429,7 @@ export default sbp('sbp/selectors/register', { }, // TODO: implement 'chelonia/contract/release' (see #828) // safer version of removeImmediately that waits to finish processing events for contractIDs - 'chelonia/contract/remove': function (contractIDs: string | string[]): Promise { + 'chelonia/contract/remove': function (contractIDs: string | string[]): Promise { const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs return Promise.all(listOfIds.map(contractID => { return sbp('chelonia/queueInvocation', contractID, [ @@ -408,7 +471,7 @@ export default sbp('sbp/selectors/register', { return events.reverse().map(b64ToStr) } }, - 'chelonia/out/eventsBetween': async function (startHash: string, endHash: string, offset: number = 0): Promise { + 'chelonia/out/eventsBetween': async function (startHash: string, endHash: string, offset = 0): Promise { if (offset < 0) { console.error('[chelonia] invalid params error: "offset" needs to be positive integer or zero') return @@ -419,7 +482,7 @@ export default sbp('sbp/selectors/register', { return events.reverse().map(b64ToStr) } }, - 'chelonia/latestContractState': async function (contractID: string): Promise { + 'chelonia/latestContractState': async function (contractID: string): Promise { const events = await sbp('chelonia/out/eventsSince', contractID, contractID) let state = {} // fast-path @@ -534,16 +597,18 @@ async function outEncryptedOrUnencryptedAction ( return message } +type Getters = Record unknown> + // The gettersProxy is what makes Vue-like getters possible. In other words, // we want to make sure that the getter functions that we defined in each // contract get passed the 'state' when a getter is accessed. // The only way to pass in the state is by creating a Proxy object that does // that for us. This allows us to maintain compatibility with Vue.js and integrate // the contract getters into the Vue-facing getters. -function gettersProxy (state: any, getters: any): { getters: any } { - const proxyGetters: any = new Proxy({}, { - get (target: any, prop: string) { - return (getters[prop] as any)(state, proxyGetters) +function gettersProxy (state: unknown, getters: Getters): { getters: Getters } { + const proxyGetters: Getters = new Proxy({} as Getters, { + get (target: Getters, prop: string) { + return (getters[prop])(state, proxyGetters) } }) return { getters: proxyGetters } diff --git a/shared/domains/chelonia/db.ts b/shared/domains/chelonia/db.ts index 7cbc46b55c..e237984f0b 100644 --- a/shared/domains/chelonia/db.ts +++ b/shared/domains/chelonia/db.ts @@ -1,11 +1,13 @@ -declare var process: any - import sbp from '@sbp/sbp' import '@sbp/okturtles.data' import '@sbp/okturtles.eventqueue' import { GIMessage } from '~/shared/domains/chelonia/GIMessage.ts' import { ChelErrorDBBadPreviousHEAD, ChelErrorDBConnection } from './errors.ts' +declare const process: { + env: Record +} + const headSuffix = '-HEAD' // NOTE: To enable persistence of log use 'sbp/selectors/overwrite' @@ -17,20 +19,20 @@ const dbPrimitiveSelectors = process.env.LIGHTWEIGHT_CLIENT === 'true' ? { 'chelonia/db/get': function (key: string): Promise { const id = sbp('chelonia/db/contractIdFromLogHEAD', key) - // @ts-ignore Property 'config' does not exist. + // @ts-expect-error Property 'config' does not exist. return Promise.resolve(id ? sbp(this.config.stateSelector).contracts[id]?.HEAD : null) }, - 'chelonia/db/set': function (key: string, value: any): Promise { return Promise.resolve(value) }, + 'chelonia/db/set': function (key: string, value: unknown): Promise { return Promise.resolve(value) }, 'chelonia/db/delete': function (): Promise { return Promise.resolve() } } : { - 'chelonia/db/get': function (key: any): any { + 'chelonia/db/get': function (key: unknown): unknown { return Promise.resolve(sbp('okTurtles.data/get', key)) }, - 'chelonia/db/set': function (key: any, value: any) { + 'chelonia/db/set': function (key: unknown, value: unknown) { return Promise.resolve(sbp('okTurtles.data/set', key, value)) }, - 'chelonia/db/delete': function (key: any) { + 'chelonia/db/delete': function (key: unknown) { return Promise.resolve(sbp('okTurtles.data/delete', key)) } } diff --git a/shared/domains/chelonia/errors.ts b/shared/domains/chelonia/errors.ts index e4a0d25652..5d79ee25fb 100644 --- a/shared/domains/chelonia/errors.ts +++ b/shared/domains/chelonia/errors.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ export class ChelErrorDBBadPreviousHEAD extends Error { // ugly boilerplate because JavaScript is stupid // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types diff --git a/shared/domains/chelonia/internals.ts b/shared/domains/chelonia/internals.ts index 342b5bb8df..53f73fd4db 100644 --- a/shared/domains/chelonia/internals.ts +++ b/shared/domains/chelonia/internals.ts @@ -6,10 +6,9 @@ import { GIOpActionUnencrypted, GIOpContract, GIOpKeyAdd, - GIOpPropSet, - GIOpValue + GIOpPropSet } from './GIMessage.ts' -import type { CheloniaConfig, CheloniaInstance, CheloniaState, ContractDefinition } from './chelonia.ts' +import type { CheloniaConfig, CheloniaInstance, CheloniaState, ContractDefinition, ContractInfo } from './chelonia.ts' import { randomIntFromRange, delay, cloneDeep, debounce, pick } from '~/frontend/model/contracts/shared/giLodash.js' import { ChelErrorUnexpected, ChelErrorUnrecoverable } from './errors.ts' @@ -18,6 +17,37 @@ import { handleFetchResult } from '~/frontend/controller/utils/misc.js' import { blake32Hash } from '~/shared/functions.ts' // import 'ses' +type BoolCallback = (...args: unknown[]) => boolean | void + +type ContractState = { + _vm?: VM +} + +type Key = { + context: string + key: string +} + +type RevertProcessParameters = { + message: GIMessage + state: CheloniaState + contractID: string + contractStateCopy: ContractState +} + +type RevertSideEffectParameters = { + message: GIMessage + state: CheloniaState + contractID: string + contractStateCopy: ContractState + stateCopy: CheloniaState +} + +type VM = { + authorizedKeys: Key[] + props: Record +} + // export const FERAL_FUNCTION = Function export default sbp('sbp/selectors/register', { @@ -33,7 +63,7 @@ export default sbp('sbp/selectors/register', { const manifestURL = `${this.config.connectionURL}/file/${manifestHash}` const manifest = await fetch(manifestURL).then(handleFetchResult('json')) const body = JSON.parse(manifest.body) - const contractInfo = (this.config.contracts.defaults.preferSlim && body.contractSlim) || body.contract + const contractInfo: ContractInfo = (this.config.contracts.defaults.preferSlim && body.contractSlim) || body.contract console.info(`[chelonia] loading contract '${contractInfo.file}'@'${body.version}' from manifest: ${manifestHash}`) const source = await fetch(`${this.config.connectionURL}/file/${contractInfo.hash}`) .then(handleFetchResult('text')) @@ -48,7 +78,7 @@ export default sbp('sbp/selectors/register', { const allowedDoms = this.config.contracts.defaults.allowedDomains .reduce(reduceAllow, {}) let contractName: string // eslint-disable-line prefer-const - const contractSBP = (selector: string, ...args: any[]) => { + const contractSBP = (selector: string, ...args: unknown[]) => { const domain = domainFromSelector(selector) if (selector.startsWith(contractName)) { selector = `${manifestHash}/${selector}` @@ -164,23 +194,24 @@ export default sbp('sbp/selectors/register', { } } }, - 'chelonia/private/in/processMessage': async function (this: CheloniaInstance, message: GIMessage, state: any) { + 'chelonia/private/in/processMessage': async function (this: CheloniaInstance, message: GIMessage, state: ContractState) { const [opT, opV] = message.op() const hash = message.hash() const contractID = message.contractID() const manifestHash = message.manifest() const config = this.config - if (!state._vm) state._vm = {} + if (!state._vm) state._vm = {} as VM + const { _vm } = state const opFns = { [GIMessage.OP_CONTRACT] (v: GIOpContract) { // TODO: shouldn't each contract have its own set of authorized keys? - if (!state._vm.authorizedKeys) state._vm.authorizedKeys = [] + if (!_vm.authorizedKeys) _vm.authorizedKeys = [] // TODO: we probably want to be pushing the de-JSON-ified key here - state._vm.authorizedKeys.push({ key: v.keyJSON, context: 'owner' }) + _vm.authorizedKeys.push({ key: v.keyJSON, context: 'owner' }) }, [GIMessage.OP_ACTION_ENCRYPTED] (v: GIOpActionEncrypted) { if (!config.skipActionProcessing) { - const decrypted = message.decryptedValue(config.decryptFn) + const decrypted = message.decryptedValue(config.decryptFn) as GIOpActionUnencrypted opFns[GIMessage.OP_ACTION_UNENCRYPTED](decrypted) } }, @@ -195,8 +226,8 @@ export default sbp('sbp/selectors/register', { }, [GIMessage.OP_PROP_DEL]: notImplemented, [GIMessage.OP_PROP_SET] (v: GIOpPropSet) { - if (!state._vm.props) state._vm.props = {} - state._vm.props[v.key] = v.value + if (!_vm.props) _vm.props = {} + _vm.props[v.key] = v.value }, [GIMessage.OP_KEY_ADD] (v: GIOpKeyAdd) { // TODO: implement this. consider creating a function so that @@ -215,14 +246,13 @@ export default sbp('sbp/selectors/register', { processOp = config.preOp(message, state) !== false && processOp } if (`preOp_${opT}` in config) { - // @ts-ignore Property 'preOp_ae' does not exist on type 'CheloniaConfig' - processOp = (config[`preOp_${opT}`] as any)(message, state) !== false && processOp + processOp = (config[`preOp_${opT}`] as BoolCallback)(message, state) !== false && processOp } if (processOp && !config.skipProcessing) { + // @ts-expect-error TS2345: Argument of type 'GIOpValue' is not assignable. opFns[opT](opV) config.postOp && config.postOp(message, state) - // @ts-ignore Property 'postOp_ae' does not exist on type 'CheloniaConfig' - ;(`postOp_${opT}` in config) && (config[`postOp_${opT}`] as any)(message, state) + ;(`postOp_${opT}` in config) && (config[`postOp_${opT}`] as BoolCallback)(message, state) } }, 'chelonia/private/in/enqueueHandleEvent': function (this: CheloniaInstance, event: GIMessage) { @@ -372,7 +402,7 @@ const handleEvent = { throw e } }, - async processMutation (this: CheloniaInstance, message: GIMessage, state: any) { + async processMutation (this: CheloniaInstance, message: GIMessage, state: CheloniaState) { const contractID = message.contractID() if (message.isFirstMessage()) { const { type } = message.opValue() as GIOpContract @@ -394,12 +424,12 @@ const handleEvent = { const contractID = message.contractID() const manifestHash = message.manifest() const hash = message.hash() - const { action, data, meta } = message.decryptedValue() + const { action, data, meta } = message.decryptedValue() as GIOpActionUnencrypted const mutation = { data, meta, hash, contractID } await sbp(`${manifestHash}/${action}/sideEffect`, mutation) } }, - revertProcess (this: CheloniaInstance, { message, state, contractID, contractStateCopy }: any) { + revertProcess (this: CheloniaInstance, { message, state, contractID, contractStateCopy }: RevertProcessParameters) { console.warn(`[chelonia] reverting mutation ${message.description()}: ${message.serialize()}. Any side effects will be skipped!`) if (!contractStateCopy) { console.warn(`[chelonia] mutation reversion on very first message for contract ${contractID}! Your contract may be too damaged to be useful and should be redeployed with bugfixes.`) @@ -407,7 +437,7 @@ const handleEvent = { } this.config.reactiveSet(state, contractID, contractStateCopy) }, - revertSideEffect (this: CheloniaInstance, { message, state, contractID, contractStateCopy, stateCopy }: any) { + revertSideEffect (this: CheloniaInstance, { message, state, contractID, contractStateCopy, stateCopy }: RevertSideEffectParameters) { console.warn(`[chelonia] reverting entire state because failed sideEffect for ${message.description()}: ${message.serialize()}`) if (!contractStateCopy) { this.config.reactiveDel(state, contractID) @@ -420,7 +450,7 @@ const handleEvent = { } } -const notImplemented = (v: any) => { +const notImplemented = (v: unknown) => { throw new Error(`chelonia: action not implemented to handle: ${JSON.stringify(v)}.`) } diff --git a/shared/functions.ts b/shared/functions.ts index 5e3ed29387..898cd666e3 100644 --- a/shared/functions.ts +++ b/shared/functions.ts @@ -7,14 +7,14 @@ import blake from 'blakejs' import { Buffer } from 'buffer' if (typeof window === 'object') { - // @ts-ignore + // @ts-expect-error TS2339 [ERROR]: Property 'Buffer' does not exist on type 'Window & typeof globalThis'. window.Buffer = Buffer } else { - // @ts-ignore + // @ts-expect-error TS7017 [ERROR]: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature. globalThis.Buffer = Buffer } -export function blake32Hash (data: any) { +export function blake32Hash (data: unknown) { // TODO: for node/electron, switch to: https://github.com/ludios/node-blake2 const uint8array = blake.blake2b(data, null, 32) // TODO: if we switch to webpack we may need: https://github.com/feross/buffer @@ -31,13 +31,18 @@ export function blake32Hash (data: any) { // These hoops might result in inconsistencies between Node.js and the frontend. export const b64ToBuf = (b64: string) => Buffer.from(b64, 'base64') export const b64ToStr = (b64: string) => b64ToBuf(b64).toString('utf8') -export const bufToB64 = (buf: any) => Buffer.from(buf).toString('base64') +export const bufToB64 = (buf: ArrayBuffer) => Buffer.from(buf).toString('base64') export const strToBuf = (str: string) => Buffer.from(str, 'utf8') export const strToB64 = (str: string) => strToBuf(str).toString('base64') -export const bytesToB64 = (ary: any) => Buffer.from(ary).toString('base64') +export const bytesToB64 = (ary: ArrayBuffer) => Buffer.from(ary).toString('base64') + +type KeyPair = { + publicKey: string + secretKey: string +} export function sign ( - { publicKey, secretKey }: any, + { publicKey, secretKey }: KeyPair, msg = 'hello!', futz = '' ) { diff --git a/shared/pubsub.test.ts b/shared/pubsub.test.ts index 9eecfd0792..a365cfae06 100644 --- a/shared/pubsub.test.ts +++ b/shared/pubsub.test.ts @@ -1,9 +1,7 @@ import { assert, - assertEquals, - assertMatch, - assertNotMatch -} from "https://deno.land/std@0.153.0/testing/asserts.ts" + assertEquals +} from 'https://deno.land/std@0.153.0/testing/asserts.ts' import '~/scripts/process-shim.ts' @@ -29,11 +27,11 @@ const createRandomDelays = (number) => { const delays1 = createRandomDelays(10) const delays2 = createRandomDelays(10) - +// Test steps must be async, but we don't always use `await` in them. +/* eslint-disable require-await */ Deno.test({ name: 'Test getNextRandomDelay()', fn: async function (tests) { - await tests.step('every delay should be longer than the previous one', async function () { // In other words, the delays should be sorted in ascending numerical order. assertEquals(delays1, [...delays1].sort((a, b) => a - b)) @@ -59,6 +57,5 @@ Deno.test({ }) }, sanitizeResources: false, - sanitizeOps: false, + sanitizeOps: false }) - diff --git a/shared/pubsub.ts b/shared/pubsub.ts index c2c854f1c2..4d6e2f7c99 100644 --- a/shared/pubsub.ts +++ b/shared/pubsub.ts @@ -1,17 +1,40 @@ -declare var process: any - import sbp from '@sbp/sbp' import '@sbp/okturtles.events' +declare const process: { + env: Record +} + type JSONType = ReturnType; // ====== Types ====== // +type Callback = (this: PubsubClient, ...args: unknown[]) => void + type Message = { [key: string]: JSONType; type: string } +type MessageHandler = (this: PubsubClient, msg: Message) => void + +type PubsubClientOptions = { + handlers?: Record + eventHandlers?: Record + logPingMessages?: boolean + manual?: boolean + maxReconnectionDelay?: number + maxRetries?: number + messageHandlers?: Record + minReconnectionDelay?: number + pingTimeout?: number + reconnectOnDisconnection?: boolean + reconnectOnOnline?: boolean + reconnectOnTimeout?: boolean + reconnectionDelayGrowFactor?: number + timeout?: number +} + // ====== Event name constants ====== // export const PUBSUB_ERROR = 'pubsub-error' @@ -43,33 +66,50 @@ export const RESPONSE_TYPE = Object.freeze({ SUCCESS: 'success' }) +// TODO: verify these are good defaults +const defaultOptions = { + logPingMessages: process.env.NODE_ENV === 'development' && !process.env.CI, + manual: false, + maxReconnectionDelay: 60000, + maxRetries: 10, + pingTimeout: 45000, + minReconnectionDelay: 500, + reconnectOnDisconnection: true, + reconnectOnOnline: true, + // Defaults to false to avoid reconnection attempts in case the server doesn't + // respond because of a failed authentication. + reconnectOnTimeout: false, + reconnectionDelayGrowFactor: 2, + timeout: 5000 +} + export class PubsubClient { - connectionTimeoutID?: number; - customEventHandlers: Record; + connectionTimeoutID?: number + customEventHandlers: Record // The current number of connection attempts that failed. // Reset to 0 upon successful connection. // Used to compute how long to wait before the next reconnection attempt. - failedConnectionAttempts: number; - isLocal: boolean; + failedConnectionAttempts: number + isLocal: boolean // True if this client has never been connected yet. - isNew: boolean; - listeners: Record; - messageHandlers: Record void>; - nextConnectionAttemptDelayID?: number; - options: any; + isNew: boolean + listeners: Record + messageHandlers: Record + nextConnectionAttemptDelayID?: number + options: typeof defaultOptions // Requested subscriptions for which we didn't receive a response yet. - pendingSubscriptionSet: Set; - pendingSyncSet: Set; - pendingUnsubscriptionSet: Set; - pingTimeoutID?: number; - shouldReconnect: boolean; + pendingSubscriptionSet: Set + pendingSyncSet: Set + pendingUnsubscriptionSet: Set + pingTimeoutID?: number + shouldReconnect: boolean // The underlying WebSocket object. // A new one is necessary for every connection or reconnection attempt. - socket: WebSocket | null = null; - subscriptionSet: Set; - url: string; + socket: WebSocket | null = null + subscriptionSet: Set + url: string - constructor (url: string, options: any = {}) { + constructor (url: string, options: PubsubClientOptions = {}) { this.customEventHandlers = options.handlers ?? {} this.failedConnectionAttempts = 0 this.isLocal = /\/\/(localhost|127\.0\.0\.1)([:?/]|$)/.test(url) @@ -95,11 +135,11 @@ export class PubsubClient { // Another benefit is the ability to patch the client protocol at runtime by // updating the client's custom event handler map. for (const name of Object.keys(defaultClientEventHandlers)) { - client.listeners[name] = (event: any) => { + client.listeners[name] = (event: Event) => { try { // Use `.call()` to pass the client via the 'this' binding. - // @ts-ignore - defaultClientEventHandlers[name as SocketEventName]?.call(client, event) + // @ts-expect-error TS2684 + defaultClientEventHandlers[name]?.call(client, event) client.customEventHandlers[name]?.call(client, event) } catch (error) { // Do not throw any error but emit an `error` event instead. @@ -182,7 +222,7 @@ export class PubsubClient { for (const name of socketEventNames) { client.socket.removeEventListener(name, client.listeners[name]) } - client.socket.close(4001, 'destroy') + client.socket.close(4001, 'terminated') } client.listeners = {} client.socket = null @@ -296,7 +336,7 @@ export class PubsubClient { * {number?} timeout=5_000 - Connection timeout duration in milliseconds. * @returns {PubSubClient} */ -export function createClient (url: string, options: any = {}): PubsubClient { +export function createClient (url: string, options: PubsubClientOptions = {}): PubsubClient { return new PubsubClient(url, options) } @@ -568,31 +608,13 @@ const defaultMessageHandlers = { } } -// TODO: verify these are good defaults -const defaultOptions = { - logPingMessages: process.env.NODE_ENV === 'development' && !process.env.CI, - pingTimeout: 45000, - maxReconnectionDelay: 60000, - maxRetries: 10, - minReconnectionDelay: 500, - reconnectOnDisconnection: true, - reconnectOnOnline: true, - // Defaults to false to avoid reconnection attempts in case the server doesn't - // respond because of a failed authentication. - reconnectOnTimeout: false, - reconnectionDelayGrowFactor: 2, - timeout: 5000 -} - const globalEventNames = ['offline', 'online'] const socketEventNames = ['close', 'error', 'message', 'open'] -type SocketEventName = 'close' | 'error' | 'message' | 'open'; - // `navigator.onLine` can give confusing false positives when `true`, // so we'll define `isDefinetelyOffline()` rather than `isOnline()` or `isOffline()`. // See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine -// @ts-ignore TS2339 [ERROR]: Property 'onLine' does not exist on type 'Navigator'. +// @ts-expect-error TS2339 [ERROR]: Property 'onLine' does not exist on type 'Navigator'. const isDefinetelyOffline = () => typeof navigator === 'object' && navigator.onLine === false // Parses and validates a received message. @@ -613,7 +635,7 @@ export const messageParser = (data: string): Message => { // Register custom SBP event listeners before the first connection. for (const name of Object.keys(defaultClientEventHandlers)) { if (name === 'error' || !socketEventNames.includes(name)) { - sbp('okTurtles.events/on', `pubsub-${name}`, (target: PubsubClient, detail: any) => { + sbp('okTurtles.events/on', `pubsub-${name}`, (target: PubsubClient, detail: unknown) => { target.listeners[name](({ type: name, target, detail } as unknown) as Event) }) } diff --git a/test/backend.test.js b/test/backend.test.js deleted file mode 100644 index 0038c4a998..0000000000 --- a/test/backend.test.js +++ /dev/null @@ -1,386 +0,0 @@ -/* eslint-env mocha */ - -import sbp from '@sbp/sbp' -import '@sbp/okturtles.events' -import '@sbp/okturtles.eventqueue' -import '~/shared/domains/chelonia/chelonia.js' -import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' -import { handleFetchResult } from '~/frontend/controller/utils/misc.js' -import { blake32Hash } from '~/shared/functions.js' -import * as Common from '@common/common.js' -import proposals from '~/frontend/model/contracts/shared/voting/proposals.js' -import { PAYMENT_PENDING, PAYMENT_TYPE_MANUAL } from '~/frontend/model/contracts/shared/payments/index.js' -import { INVITE_INITIAL_CREATOR, INVITE_EXPIRES_IN_DAYS, MAIL_TYPE_MESSAGE, PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC } from '~/frontend/model/contracts/shared/constants.js' -import { createInvite } from '~/frontend/model/contracts/shared/functions.js' -import '~/frontend/controller/namespace.js' -import chalk from 'chalk' -import { THEME_LIGHT } from '~/frontend/utils/themes.js' -import manifests from '~/frontend/model/contracts/manifests.json' - -// Necessary since we are going to use a WebSocket pubsub client in the backend. -global.WebSocket = require('ws') -const should = require('should') // eslint-disable-line - -// Remove this when dropping support for Node versions lower than v18. -const Blob = require('buffer').Blob -const fs = require('fs') -const path = require('path') -// const { PassThrough, Readable } = require('stream') - -chalk.level = 2 // for some reason it's not detecting that terminal supports colors -const { bold } = chalk - -// var unsignedMsg = sign(personas[0], 'futz') - -// TODO: replay attacks? (need server-provided challenge for `msg`?) -// nah, this should be taken care of by TLS. However, for message -// passing we should be using a forward-secure protocol. See -// MessageRelay in interface.js. - -// TODO: the request for members of a group should be made with a group -// key or a group signature. There should not be a mapping of a -// member's key to all the groups that they're in (that's unweildy -// and compromises privacy). - -const vuexState = { - currentGroupId: null, - currentChatRoomIDs: {}, - contracts: {}, // contractIDs => { type:string, HEAD:string } (for contracts we've successfully subscribed to) - pending: [], // contractIDs we've just published but haven't received back yet - loggedIn: false, // false | { username: string, identityContractID: string } - theme: THEME_LIGHT, - fontSize: 1, - increasedContrast: false, - namespaceLookups: Object.create(null), - reducedMotion: false, - appLogsFilter: ['error', 'info', 'warn'] -} - -// this is to ensure compatibility between frontend and test/backend.test.js -sbp('okTurtles.data/set', 'API_URL', process.env.API_URL) -sbp('sbp/selectors/register', { - // for handling the loggedIn metadata() in Contracts.js - 'state/vuex/state': () => { - return vuexState - } -}) - -sbp('sbp/selectors/register', { - 'backend.tests/postEntry': async function (entry) { - console.log(bold.yellow('sending entry with hash:'), entry.hash()) - const res = await sbp('chelonia/private/out/publishEvent', entry) - should(res).equal(entry.hash()) - return res - } -}) - -// uncomment this to help with debugging: -// sbp('sbp/filters/global/add', (domain, selector, data) => { -// console.log(`[sbp] ${selector}:`, data) -// }) - -describe('Full walkthrough', function () { - const users = {} - const groups = {} - - it('Should configure chelonia', async function () { - await sbp('chelonia/configure', { - connectionURL: process.env.API_URL, - stateSelector: 'state/vuex/state', - skipSideEffects: true, - connectionOptions: { - reconnectOnDisconnection: false, - reconnectOnOnline: false, - reconnectOnTimeout: false, - timeout: 3000 - }, - contracts: { - ...manifests, - defaults: { - modules: { '@common/common.js': Common }, - allowedSelectors: [ - 'state/vuex/state', 'state/vuex/commit', 'state/vuex/getters', - 'chelonia/contract/sync', 'chelonia/contract/remove', 'controller/router', - 'chelonia/queueInvocation', 'gi.actions/identity/updateLoginStateUponLogin', - 'gi.actions/chatroom/leave', 'gi.notifications/emit' - ], - allowedDomains: ['okTurtles.data', 'okTurtles.events', 'okTurtles.eventQueue'], - preferSlim: true - } - } - }) - }) - - function login (user) { - // we set this so that the metadata on subsequent messages is properly filled in - // currently group and mailbox contracts use this to determine message sender - vuexState.loggedIn = { - username: user.decryptedValue().data.attributes.username, - identityContractID: user.contractID() - } - } - - async function createIdentity (username, email, testFn) { - // append random id to username to prevent conflict across runs - // when GI_PERSIST environment variable is defined - username = `${username}-${Math.floor(Math.random() * 1000)}` - const msg = await sbp('chelonia/out/registerContract', { - contractName: 'gi.contracts/identity', - data: { - // authorizations: [Events.CanModifyAuths.dummyAuth(name)], - attributes: { username, email } - }, - hooks: { - prepublish: (message) => { message.decryptedValue(JSON.parse) }, - postpublish: (message) => { testFn && testFn(message) } - } - }) - return msg - } - function createGroup (name: string, hooks: Object = {}): Promise { - const initialInvite = createInvite({ - quantity: 60, - creator: INVITE_INITIAL_CREATOR, - expires: INVITE_EXPIRES_IN_DAYS.ON_BOARDING - }) - return sbp('chelonia/out/registerContract', { - contractName: 'gi.contracts/group', - data: { - invites: { - [initialInvite.inviteSecret]: initialInvite - }, - settings: { - // authorizations: [Events.CanModifyAuths.dummyAuth(name)], - groupName: name, - groupPicture: '', - sharedValues: 'our values', - mincomeAmount: 1000, - mincomeCurrency: 'USD', - distributionDate: new Date().toISOString(), - minimizeDistribution: true, - proposals: { - [PROPOSAL_GROUP_SETTING_CHANGE]: proposals[PROPOSAL_GROUP_SETTING_CHANGE].defaults, - [PROPOSAL_INVITE_MEMBER]: proposals[PROPOSAL_INVITE_MEMBER].defaults, - [PROPOSAL_REMOVE_MEMBER]: proposals[PROPOSAL_REMOVE_MEMBER].defaults, - [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposals[PROPOSAL_PROPOSAL_SETTING_CHANGE].defaults, - [PROPOSAL_GENERIC]: proposals[PROPOSAL_GENERIC].defaults - } - } - }, - hooks - }) - } - function createPaymentTo (to, amount, contractID, currency = 'USD'): Promise { - return sbp('chelonia/out/actionEncrypted', { - action: 'gi.contracts/group/payment', - data: { - toUser: to.decryptedValue().data.attributes.username, - amount: amount, - currency: currency, - txid: String(parseInt(Math.random() * 10000000)), - status: PAYMENT_PENDING, - paymentType: PAYMENT_TYPE_MANUAL - }, - contractID - }) - } - - async function createMailboxFor (user) { - const mailbox = await sbp('chelonia/out/registerContract', { - contractName: 'gi.contracts/mailbox', - data: {} - }) - await sbp('chelonia/out/actionEncrypted', { - action: 'gi.contracts/identity/setAttributes', - data: { mailbox: mailbox.contractID() }, - contractID: user.contractID() - }) - user.mailbox = mailbox - await sbp('chelonia/contract/sync', mailbox.contractID()) - return mailbox - } - - describe('Identity tests', function () { - it('Should create identity contracts for Alice and Bob', async function () { - users.bob = await createIdentity('bob', 'bob@okturtles.com') - users.alice = await createIdentity('alice', 'alice@okturtles.org') - // verify attribute creation and state initialization - users.bob.decryptedValue().data.attributes.username.should.match(/^bob/) - users.bob.decryptedValue().data.attributes.email.should.equal('bob@okturtles.com') - }) - - it('Should register Alice and Bob in the namespace', async function () { - const { alice, bob } = users - let res = await sbp('namespace/register', alice.decryptedValue().data.attributes.username, alice.contractID()) - // NOTE: don't rely on the return values for 'namespace/register' - // too much... in the future we might remove these checks - res.value.should.equal(alice.contractID()) - res = await sbp('namespace/register', bob.decryptedValue().data.attributes.username, bob.contractID()) - res.value.should.equal(bob.contractID()) - alice.socket = 'hello' - should(alice.socket).equal('hello') - }) - - it('Should verify namespace lookups work', async function () { - const { alice } = users - const res = await sbp('namespace/lookup', alice.decryptedValue().data.attributes.username) - res.should.equal(alice.contractID()) - const contractID = await sbp('namespace/lookup', 'susan') - should(contractID).equal(null) - }) - - it('Should open socket for Alice', async function () { - users.alice.socket = await sbp('chelonia/connect') - }) - - it('Should create mailboxes for Alice and Bob and subscribe', async function () { - // Object.values(users).forEach(async user => await createMailboxFor(user)) - await createMailboxFor(users.alice) - await createMailboxFor(users.bob) - }) - }) - - describe('Group tests', function () { - it('Should create a group & subscribe Alice', async function () { - // set user Alice as being logged in so that metadata on messages is properly set - login(users.alice) - groups.group1 = await createGroup('group1') - await sbp('chelonia/contract/sync', groups.group1.contractID()) - }) - - // NOTE: The frontend needs to use the `fetch` API instead of superagent because - // superagent doesn't support streaming, whereas fetch does. - // TODO: We should also remove superagent as a dependency since `fetch` does - // everything we need. Use fetch from now on. - it('Should get mailbox info for Bob', async function () { - // 1. look up bob's username to get his identity contract - const { bob } = users - const bobsName = bob.decryptedValue().data.attributes.username - const bobsContractId = await sbp('namespace/lookup', bobsName) - should(bobsContractId).equal(bob.contractID()) - // 2. fetch all events for his identity contract to get latest state for it - const state = await sbp('chelonia/latestContractState', bobsContractId) - console.log(bold.red('FINAL STATE:'), state) - // 3. get bob's mailbox contractID from his identity contract attributes - should(state.attributes.mailbox).equal(bob.mailbox.contractID()) - // 4. fetch the latest hash for bob's mailbox. - // we don't need latest state for it just latest hash - const res = await sbp('chelonia/out/latestHash', state.attributes.mailbox) - should(res).equal(bob.mailbox.hash()) - }) - - it("Should invite Bob to Alice's group", function (done) { - const mailbox = users.bob.mailbox - sbp('chelonia/out/actionEncrypted', { - action: 'gi.contracts/mailbox/postMessage', - data: { - from: users.bob.decryptedValue().data.attributes.username, - messageType: MAIL_TYPE_MESSAGE, - message: groups.group1.contractID() - }, - contractID: mailbox.contractID(), - hooks: { - prepublish (invite: GIMessage) { - sbp('okTurtles.events/once', invite.hash(), (contractID: string, entry: GIMessage) => { - console.debug('Bob successfully got invite!') - should(entry.decryptedValue().data.message).equal(groups.group1.contractID()) - done() - }) - } - } - }) - }) - - it('Should post an event', function () { - return createPaymentTo(users.bob, 100, groups.group1.contractID()) - }) - - it('Should sync group and verify payments in state', async function () { - await sbp('chelonia/contract/sync', groups.group1.contractID()) - should(Object.keys(vuexState[groups.group1.contractID()].payments).length).equal(1) - }) - - it('Should fail with wrong contractID', async function () { - try { - await createPaymentTo(users.bob, 100, '') - return Promise.reject(new Error("shouldn't get here!")) - } catch (e) { - return Promise.resolve() - } - }) - - // TODO: these events, as well as all messages sent over the sockets - // should all be authenticated and identified by the user's - // identity contract - }) - - describe('File upload', function () { - it('Should upload "avatar-default.png" as "multipart/form-data"', async function () { - const form = new FormData() - const filepath = './frontend/assets/images/user-avatar-default.png' - // const context = blake2bInit(32, null) - // const stream = fs.createReadStream(filepath) - // // the problem here is that we need to get the hash of the file - // // but doing so consumes the stream, invalidating it and making it - // // so that we can't simply do `form.append('data', stream)` - // // I tried creating a secondary piped stream and sending that instead, - // // however that didn't work. - // // const pass = new PassThrough() // couldn't get this or Readable to work no matter how I tried - // // So instead we save the raw buffer and send that, using a hack - // // to work around a weird bug in hapi or form-data where we have to - // // specify the filename or otherwise the backend treats the data as a string, - // // resulting in the wrong hash for some reason. By specifying `filename` the backend - // // treats it as a Buffer, and we get the correct file hash. - // // We could of course simply read the file twice, but that seems even more hackish. - // var buffer - // const hash = await new Promise((resolve, reject) => { - // stream.on('error', e => reject(e)) - // stream.on('data', chunk => { - // buffer = buffer ? Buffer.concat([buffer, chunk]) : chunk - // blake2bUpdate(context, chunk) - // }) - // stream.on('end', () => { - // const uint8array = blake2bFinal(context) - // resolve(multihash.toB58String(multihash.encode(Buffer.from(uint8array.buffer), 'blake2b-32', 32))) - // }) - // }) - // since we're just saving the buffer now, we might as well use the simpler readFileSync API - const buffer = fs.readFileSync(filepath) - const hash = blake32Hash(buffer) - console.log(`hash for ${path.basename(filepath)}: ${hash}`) - form.append('hash', hash) - form.append('data', new Blob([buffer]), path.basename(filepath)) - await fetch(`${process.env.API_URL}/file`, { method: 'POST', body: form }) - .then(handleFetchResult('text')) - .then(r => should(r).equal(`/file/${hash}`)) - }) - }) - - describe('Cleanup', function () { - it('Should destroy all opened sockets', function () { - // The code below was originally Object.values(...) but changed to .keys() - // due to a similar flow issue to https://github.com/facebook/flow/issues/2221 - Object.keys(users).forEach((userKey) => { - users[userKey].socket && users[userKey].socket.destroy() - }) - }) - }) -}) - -// Potentially useful for dealing with fetch API: -// function streamToPromise (stream, dataHandler) { -// return new Promise((resolve, reject) => { -// stream.on('data', (...args) => { -// try { dataHandler(...args) } catch (err) { reject(err) } -// }) -// stream.on('end', resolve) -// stream.on('error', reject) -// }) -// } -// see: https://github.com/bitinn/node-fetch/blob/master/test/test.js -// This used to be in the 'Should get mailbox info for Bob' test, before the -// server manually created a JSON array out of the objects being streamed. -// await streamToPromise(res.body, chunk => { -// console.log(bold.red('CHUNK:'), chunk.toString()) -// events.push(JSON.parse(chunk.toString())) -// }) diff --git a/test/backend.test.ts b/test/backend.test.ts index 4a3b52e94e..1be2983dad 100644 --- a/test/backend.test.ts +++ b/test/backend.test.ts @@ -115,9 +115,10 @@ Deno.test({ // Wait for the server to be ready. let t0 = Date.now() - let timeout = 3000 + let timeout = 30000 await new Promise((resolve, reject) => { (function ping () { + console.log(process.env.API_URL) fetch(process.env.API_URL).then(resolve).catch(() => { if (Date.now() > t0 + timeout) { reject(new Error('Test setup timed out.')) diff --git a/test/contracts/chatroom.js b/test/contracts/chatroom.js index c2fbe19935..c194739571 100644 --- a/test/contracts/chatroom.js +++ b/test/contracts/chatroom.js @@ -409,14 +409,14 @@ var objectOf = (typeObj, _scope = "Object") => { } const undefAttr = typeAttrs.find((property) => { const propertyTypeFn = typeObj[property]; - return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); }); if (undefAttr) { throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); } const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { const typeFn = typeObj[key]; - if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { return Object.assign(acc, {}); } else { return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); @@ -425,7 +425,10 @@ var objectOf = (typeObj, _scope = "Object") => { return typeAttrs.reduce(reducer, {}); } object2.type = () => { - const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); return `{| ${props.join(",\n ")} |}`; diff --git a/test/contracts/chatroom.js.map b/test/contracts/chatroom.js.map index 2aba20bd13..de86a0719b 100644 --- a/test/contracts/chatroom.js.map +++ b/test/contracts/chatroom.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/shared/time.js", "../../frontend/views/utils/misc.js", "../../frontend/model/contracts/shared/functions.js", "../../frontend/model/contracts/shared/nativeNotification.js", "../../frontend/model/contracts/chatroom.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nexport function logExceptNavigationDuplicated (err ) {\n err.name !== 'NavigationDuplicated' && console.error(err)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { INVITE_STATUS, MESSAGE_TYPES } from './constants.js'\nimport { DAYS_MILLIS } from './time.js'\nimport { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js'\n\n// !!!!!!!!!!!!!!!\n// !! IMPORTANT !!\n// !!!!!!!!!!!!!!!\n//\n// DO NOT CHANGE THE LOGIC TO ANY OF THESE FUNCTIONS!\n// INSTEAD, CREATE NEW FUNCTIONS WITH DIFFERENT NAMES\n// AND USE THOSE INSTEAD!\n//\n// THIS IS A CONSEQUENCE OF SHARING THIS CODE WITH THE REST OF THE APP.\n// IF YOU DO NOT NEED TO SHARE CODE WITH THE REST OF THE APP (AND CAN\n// KEEP IT WITHIN THE CONTRACT ONLY), THEN YOU DON'T NEED TO WORRY ABOUT\n// THIS, AND SHOULD INCLUDE THOSE FUNCTIONS (WITHOUT EXPORTING THEM),\n// DIRECTLY IN YOUR CONTRACT DEFINITION FILE. THEN YOU CAN MODIFY\n// THEM AS MUCH AS YOU LIKE (and generate new contract versions out of them).\n\n// group.js related\n\nexport function createInvite ({ quantity = 1, creator, expires, invitee } \n \n ) \n \n \n \n \n \n \n \n {\n return {\n inviteSecret: `${parseInt(Math.random() * 10000)}`, // TODO: this\n quantity,\n creator,\n invitee,\n status: INVITE_STATUS.VALID,\n responses: {}, // { bob: true } list of usernames that accepted the invite.\n expires: Date.now() + DAYS_MILLIS * expires\n }\n}\n\n// chatroom.js related\n\nexport function createMessage ({ meta, data, hash, state } \n \n ) {\n const { type, text, replyingMessage } = data\n const { createdDate } = meta\n\n let newMessage = {\n type,\n datetime: new Date(createdDate).toISOString(),\n id: hash,\n from: meta.username\n }\n\n if (type === MESSAGE_TYPES.TEXT) {\n newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }\n } else if (type === MESSAGE_TYPES.POLL) {\n // TODO: Poll message creation\n } else if (type === MESSAGE_TYPES.NOTIFICATION) {\n const params = {\n channelName: state?.attributes.name,\n channelDescription: state?.attributes.description,\n ...data.notification\n }\n delete params.type\n newMessage = {\n ...newMessage,\n notification: { type: data.notification.type, params }\n }\n } else if (type === MESSAGE_TYPES.INTERACTIVE) {\n // TODO: Interactive message creation for proposals\n }\n return newMessage\n}\n\nexport async function leaveChatRoom ({ contractID } \n \n ) {\n const rootState = sbp('state/vuex/state')\n const rootGetters = sbp('state/vuex/getters')\n if (contractID === rootGetters.currentChatRoomId) {\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {\n groupId: rootState.currentGroupId\n })\n const curRouteName = sbp('controller/router').history.current.name\n if (curRouteName === 'GroupChat' || curRouteName === 'GroupChatConversation') {\n await sbp('controller/router')\n .push({ name: 'GroupChatConversation', params: { chatRoomId: rootGetters.currentChatRoomId } })\n .catch(logExceptNavigationDuplicated)\n }\n }\n\n sbp('state/vuex/commit', 'deleteChatRoomUnread', { chatRoomId: contractID })\n sbp('state/vuex/commit', 'deleteChatRoomScrollPosition', { chatRoomId: contractID })\n\n // NOTE: make sure *not* to await on this, since that can cause\n // a potential deadlock. See same warning in sideEffect for\n // 'gi.contracts/group/removeMember'\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e)\n })\n}\n\nexport function findMessageIdx (id , messages ) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].id === id) {\n return i\n }\n }\n return -1\n}\n\nexport function makeMentionFromUsername (username ) \n \n {\n return {\n me: `@${username}`,\n all: '@all'\n }\n}\n", "'use strict'\nimport sbp from '@sbp/sbp'\n\n// NOTE: since these functions don't modify contract state, it should\n// be safe to modify them without worrying about version conflicts.\n\nexport async function requestNotificationPermission (force = false) {\n if (typeof Notification === 'undefined') {\n return null\n }\n if (force || Notification.permission === 'default') {\n try {\n sbp('state/vuex/commit', 'setNotificationEnabled', await Notification.requestPermission() === 'granted')\n } catch (e) {\n console.error('requestNotificationPermission:', e.message)\n return null\n }\n }\n return Notification.permission\n}\n\nexport function makeNotification ({ title, body, icon, path } \n \n ) {\n const notificationEnabled = sbp('state/vuex/state').notificationEnabled\n if (typeof Notification === 'undefined' || Notification.permission !== 'granted' || !notificationEnabled) {\n return\n }\n\n const notification = new Notification(title, { body, icon })\n if (path) {\n notification.onclick = function (event) {\n event.preventDefault()\n sbp('controller/router').push({ path }).catch(console.warn)\n }\n }\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, L } from '@common/common.js'\nimport { merge, cloneDeep } from './shared/giLodash.js'\nimport {\n CHATROOM_NAME_LIMITS_IN_CHARS,\n CHATROOM_DESCRIPTION_LIMITS_IN_CHARS,\n CHATROOM_ACTIONS_PER_PAGE,\n CHATROOM_MESSAGES_PER_PAGE,\n MESSAGE_TYPES,\n MESSAGE_NOTIFICATIONS,\n CHATROOM_MESSAGE_ACTION,\n MESSAGE_RECEIVE\n} from './shared/constants.js'\nimport { chatRoomAttributesType, messageType } from './shared/types.js'\nimport { createMessage, leaveChatRoom, findMessageIdx, makeMentionFromUsername } from './shared/functions.js'\nimport { makeNotification } from './shared/nativeNotification.js'\nimport { objectOf, string, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction createNotificationData (\n notificationType ,\n moreParams = {}\n) {\n return {\n type: MESSAGE_TYPES.NOTIFICATION,\n notification: {\n type: notificationType,\n ...moreParams\n }\n }\n}\n\nfunction emitMessageEvent ({ contractID, hash } \n \n \n ) {\n sbp('okTurtles.events/emit', `${CHATROOM_MESSAGE_ACTION}-${contractID}`, { hash })\n}\n\nfunction addMention ({ contractID, messageId, datetime, text, username, chatRoomName } \n \n \n \n \n \n \n ) {\n /**\n * If 'READY_TO_JOIN_CHATROOM' is false, it means not syncing chatroom\n */\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n return\n }\n\n sbp('state/vuex/commit', 'addChatRoomUnreadMention', {\n chatRoomId: contractID,\n messageId,\n createdDate: datetime\n })\n\n const rootGetters = sbp('state/vuex/getters')\n const groupID = rootGetters.groupIdFromChatRoomId(contractID)\n const path = `/group-chat/${contractID}`\n\n makeNotification({\n title: `# ${chatRoomName}`,\n body: text,\n icon: rootGetters.globalProfile2(groupID, username).picture,\n path\n })\n\n sbp('okTurtles.events/emit', MESSAGE_RECEIVE)\n}\n\nfunction deleteMention ({ contractID, messageId } \n \n ) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadMention', { chatRoomId: contractID, messageId })\n}\n\nfunction updateUnreadPosition ({ contractID, hash, createdDate } \n \n ) {\n sbp('state/vuex/commit', 'setChatRoomUnreadSince', {\n chatRoomId: contractID,\n messageId: hash,\n createdDate\n })\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/chatroom',\n metadata: {\n validate: objectOf({\n createdDate: string, // action created date\n username: string, // action creator\n identityContractID: string // action creator identityContractID\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n getters: {\n currentChatRoomState (state) {\n return state\n },\n chatRoomSettings (state, getters) {\n return getters.currentChatRoomState.settings || {}\n },\n chatRoomAttributes (state, getters) {\n return getters.currentChatRoomState.attributes || {}\n },\n chatRoomUsers (state, getters) {\n return getters.currentChatRoomState.users || {}\n },\n chatRoomLatestMessages (state, getters) {\n return getters.currentChatRoomState.messages || []\n }\n },\n actions: {\n // This is the constructor of Chat contract\n 'gi.contracts/chatroom': {\n validate: objectOf({\n attributes: chatRoomAttributesType\n }),\n process ({ meta, data }, { state }) {\n const initialState = merge({\n settings: {\n actionsPerPage: CHATROOM_ACTIONS_PER_PAGE,\n messagesPerPage: CHATROOM_MESSAGES_PER_PAGE,\n maxNameLength: CHATROOM_NAME_LIMITS_IN_CHARS,\n maxDescriptionLength: CHATROOM_DESCRIPTION_LIMITS_IN_CHARS\n },\n attributes: {\n creator: meta.username,\n deletedDate: null,\n archivedDate: null\n },\n users: {},\n messages: []\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/chatroom/join': {\n validate: objectOf({\n username: string // username of joining member\n }),\n process ({ data, meta, hash }, { state }) {\n const { username } = data\n if (!state.saveMessage && state.users[username]) {\n // this can happen when we're logging in on another machine, and also in other circumstances\n console.warn('Can not join the chatroom which you are already part of')\n return\n }\n\n Vue.set(state.users, username, { joinedDate: meta.createdDate })\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = username === meta.username ? MESSAGE_NOTIFICATIONS.JOIN_MEMBER : MESSAGE_NOTIFICATIONS.ADD_MEMBER\n const notificationData = createNotificationData(\n notificationType,\n notificationType === MESSAGE_NOTIFICATIONS.ADD_MEMBER ? { username } : {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM') || // Join by himself or Login in another device\n sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID') === contractID) { // Be added by another\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/rename': {\n validate: objectOf({\n name: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'name', data.name)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_NAME, {})\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/changeDescription': {\n validate: objectOf({\n description: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'description', data.description)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(\n MESSAGE_NOTIFICATIONS.UPDATE_DESCRIPTION, {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/leave': {\n validate: objectOf({\n username: optional(string), // coming from the gi.contracts/group/leaveChatRoom\n member: string // username to be removed\n }),\n process ({ data, meta, hash }, { state }) {\n const { member } = data\n const isKicked = data.username && member !== data.username\n if (!state.saveMessage && !state.users[member]) {\n throw new Error(`Can not leave the chatroom which ${member} are not part of`)\n }\n Vue.delete(state.users, member)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = !isKicked ? MESSAGE_NOTIFICATIONS.LEAVE_MEMBER : MESSAGE_NOTIFICATIONS.KICK_MEMBER\n const notificationData = createNotificationData(notificationType, isKicked ? { username: member } : {})\n const newMessage = createMessage({\n meta: isKicked ? meta : { ...meta, username: member },\n hash,\n data: notificationData,\n state\n })\n state.messages.push(newMessage)\n },\n sideEffect ({ data, hash, contractID, meta }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (data.member === rootState.loggedIn.username) {\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/delete': {\n validate: (data, { state, meta }) => {\n if (state.attributes.creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state, rootState }) {\n Vue.set(state.attributes, 'deletedDate', meta.createdDate)\n for (const username in state.users) {\n Vue.delete(state.users, username)\n }\n },\n sideEffect ({ meta, contractID }, { state }) {\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n },\n 'gi.contracts/chatroom/addMessage': {\n validate: messageType,\n process ({ data, meta, hash }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const pendingMsg = state.messages.find(msg => msg.id === hash && msg.pending)\n if (pendingMsg) {\n delete pendingMsg.pending\n } else {\n state.messages.push(createMessage({ meta, data, hash, state }))\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { state, getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const newMessage = createMessage({ meta, data, hash, state })\n const mentions = makeMentionFromUsername(me)\n if (data.type === MESSAGE_TYPES.TEXT &&\n (newMessage.text.includes(mentions.me) || newMessage.text.includes(mentions.all))) {\n addMention({\n contractID,\n messageId: newMessage.id,\n datetime: newMessage.datetime,\n text: newMessage.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n }\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/editMessage': {\n validate: (data, { state, meta }) => {\n objectOf({\n id: string,\n createdDate: string,\n text: string\n })(data)\n // TODO: Actually NOT SURE it's needed to check if the meta.username === message.from\n // there is no messagess in vuex state\n // to check if the meta.username is creator seems like too heavy\n },\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0 && meta.username === state.messages[msgIndex].from) {\n state.messages[msgIndex].text = data.text\n state.messages[msgIndex].updatedDate = meta.createdDate\n if (state.saveMessage && state.messages[msgIndex].pending) {\n delete state.messages[msgIndex].pending\n }\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const isAlreadyAdded = rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)\n const mentions = makeMentionFromUsername(me)\n const isIncludeMention = data.text.includes(mentions.me) || data.text.includes(mentions.all)\n if (!isAlreadyAdded && isIncludeMention) {\n addMention({\n contractID,\n messageId: data.id,\n /*\n * the following datetime is the time when the message(which made mention) is created\n * the reason why it is it instead of datetime when the mention created is because\n * it is compared to the datetime of other messages when user scrolls\n * to decide if it should be removed from the list of mentions or not\n */\n datetime: data.createdDate,\n text: data.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n } else if (isAlreadyAdded && !isIncludeMention) {\n deleteMention({ contractID, messageId: data.id })\n }\n }\n },\n 'gi.contracts/chatroom/deleteMessage': {\n validate: objectOf({\n id: string\n }),\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0) {\n state.messages.splice(msgIndex, 1)\n }\n // filter replied messages and check if the current message is original\n for (const message of state.messages) {\n if (message.replyingMessage?.id === data.id) {\n message.replyingMessage.id = null\n message.replyingMessage.text = 'Original message was removed.'\n }\n }\n },\n sideEffect ({ data, contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (rootState.chatRoomScrollPosition[contractID] === data.id) {\n sbp('state/vuex/commit', 'setChatRoomScrollPosition', {\n chatRoomId: contractID, messageId: null\n })\n }\n\n if (rootState.chatRoomUnread[contractID].since.messageId === data.id) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadSince', {\n chatRoomId: contractID,\n deletedDate: meta.createdDate\n })\n }\n\n if (me === meta.username) {\n return\n }\n if (rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)) {\n deleteMention({ contractID, messageId: data.id })\n }\n\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/makeEmotion': {\n validate: objectOf({\n id: string,\n emoticon: string\n }),\n process ({ data, meta, contractID }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const { id, emoticon } = data\n const msgIndex = findMessageIdx(id, state.messages)\n if (msgIndex >= 0) {\n let emoticons = cloneDeep(state.messages[msgIndex].emoticons || {})\n if (emoticons[emoticon]) {\n const alreadyAdded = emoticons[emoticon].indexOf(meta.username)\n if (alreadyAdded >= 0) {\n emoticons[emoticon].splice(alreadyAdded, 1)\n if (!emoticons[emoticon].length) {\n delete emoticons[emoticon]\n if (!Object.keys(emoticons).length) {\n emoticons = null\n }\n }\n } else {\n emoticons[emoticon].push(meta.username)\n }\n } else {\n emoticons[emoticon] = [meta.username]\n }\n if (emoticons) {\n Vue.set(state.messages[msgIndex], 'emoticons', emoticons)\n } else {\n Vue.delete(state.messages[msgIndex], 'emoticons')\n }\n }\n },\n sideEffect ({ contractID, hash }) {\n emitMessageEvent({ contractID, hash })\n }\n }\n }\n})\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nexport function logExceptNavigationDuplicated (err ) {\n err.name !== 'NavigationDuplicated' && console.error(err)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { INVITE_STATUS, MESSAGE_TYPES } from './constants.js'\nimport { DAYS_MILLIS } from './time.js'\nimport { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js'\n\n// !!!!!!!!!!!!!!!\n// !! IMPORTANT !!\n// !!!!!!!!!!!!!!!\n//\n// DO NOT CHANGE THE LOGIC TO ANY OF THESE FUNCTIONS!\n// INSTEAD, CREATE NEW FUNCTIONS WITH DIFFERENT NAMES\n// AND USE THOSE INSTEAD!\n//\n// THIS IS A CONSEQUENCE OF SHARING THIS CODE WITH THE REST OF THE APP.\n// IF YOU DO NOT NEED TO SHARE CODE WITH THE REST OF THE APP (AND CAN\n// KEEP IT WITHIN THE CONTRACT ONLY), THEN YOU DON'T NEED TO WORRY ABOUT\n// THIS, AND SHOULD INCLUDE THOSE FUNCTIONS (WITHOUT EXPORTING THEM),\n// DIRECTLY IN YOUR CONTRACT DEFINITION FILE. THEN YOU CAN MODIFY\n// THEM AS MUCH AS YOU LIKE (and generate new contract versions out of them).\n\n// group.js related\n\nexport function createInvite ({ quantity = 1, creator, expires, invitee } \n \n ) \n \n \n \n \n \n \n \n {\n return {\n inviteSecret: `${parseInt(Math.random() * 10000)}`, // TODO: this\n quantity,\n creator,\n invitee,\n status: INVITE_STATUS.VALID,\n responses: {}, // { bob: true } list of usernames that accepted the invite.\n expires: Date.now() + DAYS_MILLIS * expires\n }\n}\n\n// chatroom.js related\n\nexport function createMessage ({ meta, data, hash, state } \n \n ) {\n const { type, text, replyingMessage } = data\n const { createdDate } = meta\n\n let newMessage = {\n type,\n datetime: new Date(createdDate).toISOString(),\n id: hash,\n from: meta.username\n }\n\n if (type === MESSAGE_TYPES.TEXT) {\n newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }\n } else if (type === MESSAGE_TYPES.POLL) {\n // TODO: Poll message creation\n } else if (type === MESSAGE_TYPES.NOTIFICATION) {\n const params = {\n channelName: state?.attributes.name,\n channelDescription: state?.attributes.description,\n ...data.notification\n }\n delete params.type\n newMessage = {\n ...newMessage,\n notification: { type: data.notification.type, params }\n }\n } else if (type === MESSAGE_TYPES.INTERACTIVE) {\n // TODO: Interactive message creation for proposals\n }\n return newMessage\n}\n\nexport async function leaveChatRoom ({ contractID } \n \n ) {\n const rootState = sbp('state/vuex/state')\n const rootGetters = sbp('state/vuex/getters')\n if (contractID === rootGetters.currentChatRoomId) {\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {\n groupId: rootState.currentGroupId\n })\n const curRouteName = sbp('controller/router').history.current.name\n if (curRouteName === 'GroupChat' || curRouteName === 'GroupChatConversation') {\n await sbp('controller/router')\n .push({ name: 'GroupChatConversation', params: { chatRoomId: rootGetters.currentChatRoomId } })\n .catch(logExceptNavigationDuplicated)\n }\n }\n\n sbp('state/vuex/commit', 'deleteChatRoomUnread', { chatRoomId: contractID })\n sbp('state/vuex/commit', 'deleteChatRoomScrollPosition', { chatRoomId: contractID })\n\n // NOTE: make sure *not* to await on this, since that can cause\n // a potential deadlock. See same warning in sideEffect for\n // 'gi.contracts/group/removeMember'\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e)\n })\n}\n\nexport function findMessageIdx (id , messages ) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].id === id) {\n return i\n }\n }\n return -1\n}\n\nexport function makeMentionFromUsername (username ) \n \n {\n return {\n me: `@${username}`,\n all: '@all'\n }\n}\n", "'use strict'\nimport sbp from '@sbp/sbp'\n\n// NOTE: since these functions don't modify contract state, it should\n// be safe to modify them without worrying about version conflicts.\n\nexport async function requestNotificationPermission (force = false) {\n if (typeof Notification === 'undefined') {\n return null\n }\n if (force || Notification.permission === 'default') {\n try {\n sbp('state/vuex/commit', 'setNotificationEnabled', await Notification.requestPermission() === 'granted')\n } catch (e) {\n console.error('requestNotificationPermission:', e.message)\n return null\n }\n }\n return Notification.permission\n}\n\nexport function makeNotification ({ title, body, icon, path } \n \n ) {\n const notificationEnabled = sbp('state/vuex/state').notificationEnabled\n if (typeof Notification === 'undefined' || Notification.permission !== 'granted' || !notificationEnabled) {\n return\n }\n\n const notification = new Notification(title, { body, icon })\n if (path) {\n notification.onclick = function (event) {\n event.preventDefault()\n sbp('controller/router').push({ path }).catch(console.warn)\n }\n }\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, L } from '@common/common.js'\nimport { merge, cloneDeep } from './shared/giLodash.js'\nimport {\n CHATROOM_NAME_LIMITS_IN_CHARS,\n CHATROOM_DESCRIPTION_LIMITS_IN_CHARS,\n CHATROOM_ACTIONS_PER_PAGE,\n CHATROOM_MESSAGES_PER_PAGE,\n MESSAGE_TYPES,\n MESSAGE_NOTIFICATIONS,\n CHATROOM_MESSAGE_ACTION,\n MESSAGE_RECEIVE\n} from './shared/constants.js'\nimport { chatRoomAttributesType, messageType } from './shared/types.js'\nimport { createMessage, leaveChatRoom, findMessageIdx, makeMentionFromUsername } from './shared/functions.js'\nimport { makeNotification } from './shared/nativeNotification.js'\nimport { objectOf, string, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction createNotificationData (\n notificationType ,\n moreParams = {}\n) {\n return {\n type: MESSAGE_TYPES.NOTIFICATION,\n notification: {\n type: notificationType,\n ...moreParams\n }\n }\n}\n\nfunction emitMessageEvent ({ contractID, hash } \n \n \n ) {\n sbp('okTurtles.events/emit', `${CHATROOM_MESSAGE_ACTION}-${contractID}`, { hash })\n}\n\nfunction addMention ({ contractID, messageId, datetime, text, username, chatRoomName } \n \n \n \n \n \n \n ) {\n /**\n * If 'READY_TO_JOIN_CHATROOM' is false, it means not syncing chatroom\n */\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n return\n }\n\n sbp('state/vuex/commit', 'addChatRoomUnreadMention', {\n chatRoomId: contractID,\n messageId,\n createdDate: datetime\n })\n\n const rootGetters = sbp('state/vuex/getters')\n const groupID = rootGetters.groupIdFromChatRoomId(contractID)\n const path = `/group-chat/${contractID}`\n\n makeNotification({\n title: `# ${chatRoomName}`,\n body: text,\n icon: rootGetters.globalProfile2(groupID, username).picture,\n path\n })\n\n sbp('okTurtles.events/emit', MESSAGE_RECEIVE)\n}\n\nfunction deleteMention ({ contractID, messageId } \n \n ) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadMention', { chatRoomId: contractID, messageId })\n}\n\nfunction updateUnreadPosition ({ contractID, hash, createdDate } \n \n ) {\n sbp('state/vuex/commit', 'setChatRoomUnreadSince', {\n chatRoomId: contractID,\n messageId: hash,\n createdDate\n })\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/chatroom',\n metadata: {\n validate: objectOf({\n createdDate: string, // action created date\n username: string, // action creator\n identityContractID: string // action creator identityContractID\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n getters: {\n currentChatRoomState (state) {\n return state\n },\n chatRoomSettings (state, getters) {\n return getters.currentChatRoomState.settings || {}\n },\n chatRoomAttributes (state, getters) {\n return getters.currentChatRoomState.attributes || {}\n },\n chatRoomUsers (state, getters) {\n return getters.currentChatRoomState.users || {}\n },\n chatRoomLatestMessages (state, getters) {\n return getters.currentChatRoomState.messages || []\n }\n },\n actions: {\n // This is the constructor of Chat contract\n 'gi.contracts/chatroom': {\n validate: objectOf({\n attributes: chatRoomAttributesType\n }),\n process ({ meta, data }, { state }) {\n const initialState = merge({\n settings: {\n actionsPerPage: CHATROOM_ACTIONS_PER_PAGE,\n messagesPerPage: CHATROOM_MESSAGES_PER_PAGE,\n maxNameLength: CHATROOM_NAME_LIMITS_IN_CHARS,\n maxDescriptionLength: CHATROOM_DESCRIPTION_LIMITS_IN_CHARS\n },\n attributes: {\n creator: meta.username,\n deletedDate: null,\n archivedDate: null\n },\n users: {},\n messages: []\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/chatroom/join': {\n validate: objectOf({\n username: string // username of joining member\n }),\n process ({ data, meta, hash }, { state }) {\n const { username } = data\n if (!state.saveMessage && state.users[username]) {\n // this can happen when we're logging in on another machine, and also in other circumstances\n console.warn('Can not join the chatroom which you are already part of')\n return\n }\n\n Vue.set(state.users, username, { joinedDate: meta.createdDate })\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = username === meta.username ? MESSAGE_NOTIFICATIONS.JOIN_MEMBER : MESSAGE_NOTIFICATIONS.ADD_MEMBER\n const notificationData = createNotificationData(\n notificationType,\n notificationType === MESSAGE_NOTIFICATIONS.ADD_MEMBER ? { username } : {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM') || // Join by himself or Login in another device\n sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID') === contractID) { // Be added by another\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/rename': {\n validate: objectOf({\n name: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'name', data.name)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_NAME, {})\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/changeDescription': {\n validate: objectOf({\n description: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'description', data.description)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(\n MESSAGE_NOTIFICATIONS.UPDATE_DESCRIPTION, {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/leave': {\n validate: objectOf({\n username: optional(string), // coming from the gi.contracts/group/leaveChatRoom\n member: string // username to be removed\n }),\n process ({ data, meta, hash }, { state }) {\n const { member } = data\n const isKicked = data.username && member !== data.username\n if (!state.saveMessage && !state.users[member]) {\n throw new Error(`Can not leave the chatroom which ${member} are not part of`)\n }\n Vue.delete(state.users, member)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = !isKicked ? MESSAGE_NOTIFICATIONS.LEAVE_MEMBER : MESSAGE_NOTIFICATIONS.KICK_MEMBER\n const notificationData = createNotificationData(notificationType, isKicked ? { username: member } : {})\n const newMessage = createMessage({\n meta: isKicked ? meta : { ...meta, username: member },\n hash,\n data: notificationData,\n state\n })\n state.messages.push(newMessage)\n },\n sideEffect ({ data, hash, contractID, meta }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (data.member === rootState.loggedIn.username) {\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/delete': {\n validate: (data, { state, meta }) => {\n if (state.attributes.creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state, rootState }) {\n Vue.set(state.attributes, 'deletedDate', meta.createdDate)\n for (const username in state.users) {\n Vue.delete(state.users, username)\n }\n },\n sideEffect ({ meta, contractID }, { state }) {\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n },\n 'gi.contracts/chatroom/addMessage': {\n validate: messageType,\n process ({ data, meta, hash }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const pendingMsg = state.messages.find(msg => msg.id === hash && msg.pending)\n if (pendingMsg) {\n delete pendingMsg.pending\n } else {\n state.messages.push(createMessage({ meta, data, hash, state }))\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { state, getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const newMessage = createMessage({ meta, data, hash, state })\n const mentions = makeMentionFromUsername(me)\n if (data.type === MESSAGE_TYPES.TEXT &&\n (newMessage.text.includes(mentions.me) || newMessage.text.includes(mentions.all))) {\n addMention({\n contractID,\n messageId: newMessage.id,\n datetime: newMessage.datetime,\n text: newMessage.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n }\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/editMessage': {\n validate: (data, { state, meta }) => {\n objectOf({\n id: string,\n createdDate: string,\n text: string\n })(data)\n // TODO: Actually NOT SURE it's needed to check if the meta.username === message.from\n // there is no messagess in vuex state\n // to check if the meta.username is creator seems like too heavy\n },\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0 && meta.username === state.messages[msgIndex].from) {\n state.messages[msgIndex].text = data.text\n state.messages[msgIndex].updatedDate = meta.createdDate\n if (state.saveMessage && state.messages[msgIndex].pending) {\n delete state.messages[msgIndex].pending\n }\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const isAlreadyAdded = rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)\n const mentions = makeMentionFromUsername(me)\n const isIncludeMention = data.text.includes(mentions.me) || data.text.includes(mentions.all)\n if (!isAlreadyAdded && isIncludeMention) {\n addMention({\n contractID,\n messageId: data.id,\n /*\n * the following datetime is the time when the message(which made mention) is created\n * the reason why it is it instead of datetime when the mention created is because\n * it is compared to the datetime of other messages when user scrolls\n * to decide if it should be removed from the list of mentions or not\n */\n datetime: data.createdDate,\n text: data.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n } else if (isAlreadyAdded && !isIncludeMention) {\n deleteMention({ contractID, messageId: data.id })\n }\n }\n },\n 'gi.contracts/chatroom/deleteMessage': {\n validate: objectOf({\n id: string\n }),\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0) {\n state.messages.splice(msgIndex, 1)\n }\n // filter replied messages and check if the current message is original\n for (const message of state.messages) {\n if (message.replyingMessage?.id === data.id) {\n message.replyingMessage.id = null\n message.replyingMessage.text = 'Original message was removed.'\n }\n }\n },\n sideEffect ({ data, contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (rootState.chatRoomScrollPosition[contractID] === data.id) {\n sbp('state/vuex/commit', 'setChatRoomScrollPosition', {\n chatRoomId: contractID, messageId: null\n })\n }\n\n if (rootState.chatRoomUnread[contractID].since.messageId === data.id) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadSince', {\n chatRoomId: contractID,\n deletedDate: meta.createdDate\n })\n }\n\n if (me === meta.username) {\n return\n }\n if (rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)) {\n deleteMention({ contractID, messageId: data.id })\n }\n\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/makeEmotion': {\n validate: objectOf({\n id: string,\n emoticon: string\n }),\n process ({ data, meta, contractID }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const { id, emoticon } = data\n const msgIndex = findMessageIdx(id, state.messages)\n if (msgIndex >= 0) {\n let emoticons = cloneDeep(state.messages[msgIndex].emoticons || {})\n if (emoticons[emoticon]) {\n const alreadyAdded = emoticons[emoticon].indexOf(meta.username)\n if (alreadyAdded >= 0) {\n emoticons[emoticon].splice(alreadyAdded, 1)\n if (!emoticons[emoticon].length) {\n delete emoticons[emoticon]\n if (!Object.keys(emoticons).length) {\n emoticons = null\n }\n }\n } else {\n emoticons[emoticon].push(meta.username)\n }\n } else {\n emoticons[emoticon] = [meta.username]\n }\n if (emoticons) {\n Vue.set(state.messages[msgIndex], 'emoticons', emoticons)\n } else {\n Vue.delete(state.messages[msgIndex], 'emoticons')\n }\n }\n },\n sideEffect ({ contractID, hash }) {\n emitMessageEvent({ contractID, hash })\n }\n }\n }\n})\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/group.js b/test/contracts/group.js index 33146eda3d..f152ac509e 100644 --- a/test/contracts/group.js +++ b/test/contracts/group.js @@ -432,14 +432,14 @@ var objectOf = (typeObj, _scope = "Object") => { } const undefAttr = typeAttrs.find((property) => { const propertyTypeFn = typeObj[property]; - return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); }); if (undefAttr) { throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); } const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { const typeFn = typeObj[key]; - if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { return Object.assign(acc, {}); } else { return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); @@ -448,7 +448,10 @@ var objectOf = (typeObj, _scope = "Object") => { return typeAttrs.reduce(reducer, {}); } object2.type = () => { - const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); return `{| ${props.join(",\n ")} |}`; @@ -1539,6 +1542,7 @@ module_default("chelonia/defineContract", { objectOf({ member: string, reason: optional(string), + automated: optional(boolean), proposalHash: optional(string), proposalPayload: optional(objectOf({ secret: string diff --git a/test/contracts/group.js.map b/test/contracts/group.js.map index bdee49dc70..a49aacc94f 100644 --- a/test/contracts/group.js.map +++ b/test/contracts/group.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/common/errors.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/shared/voting/rules.js", "../../frontend/model/contracts/shared/time.js", "../../frontend/model/contracts/shared/voting/proposals.js", "../../frontend/model/contracts/shared/payments/index.js", "../../frontend/model/contracts/shared/distribution/mincome-proportional.js", "../../frontend/model/contracts/shared/distribution/payments-minimizer.js", "../../frontend/model/contracts/shared/currencies.js", "../../frontend/model/contracts/shared/distribution/distribution.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/group.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nexport class GIErrorIgnoreAndBan extends Error {\n // ugly boilerplate because JavaScript is stupid\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types\n constructor (...params ) {\n super(...params)\n this.name = 'GIErrorIgnoreAndBan'\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n\n// Used to throw human readable errors on UI.\nexport class GIErrorUIRuntimeError extends Error {\n constructor (...params ) {\n super(...params)\n // this.name = this.constructor.name\n this.name = 'GIErrorUIRuntimeError' // string literal so minifier doesn't overwrite\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n", "'use strict'\n\n \n \n \n \n\nexport default function mincomeProportional (haveNeeds ) {\n let totalHave = 0\n let totalNeed = 0\n const havers = []\n const needers = []\n for (const haveNeed of haveNeeds) {\n if (haveNeed.haveNeed > 0) {\n havers.push(haveNeed)\n totalHave += haveNeed.haveNeed\n } else if (haveNeed.haveNeed < 0) {\n needers.push(haveNeed)\n totalNeed += Math.abs(haveNeed.haveNeed)\n }\n }\n const totalPercent = Math.min(1, totalNeed / totalHave)\n const payments = []\n for (const haver of havers) {\n const distributionAmount = totalPercent * haver.haveNeed\n for (const needer of needers) {\n const belowPercentage = Math.abs(needer.haveNeed) / totalNeed\n payments.push({\n amount: distributionAmount * belowPercentage,\n from: haver.name,\n to: needer.name\n })\n }\n }\n return payments\n}\n", "'use strict'\n\n// greedy algorithm responsible for \"balancing\" payments\n// such that the least number of payments are made.\nexport default function minimizeTotalPaymentsCount (\n distribution \n) {\n const neederTotalReceived = {}\n const haverTotalHave = {}\n const haversSorted = []\n const needersSorted = []\n const minimizedDistribution = []\n for (const todo of distribution) {\n neederTotalReceived[todo.to] = (neederTotalReceived[todo.to] || 0) + todo.amount\n haverTotalHave[todo.from] = (haverTotalHave[todo.from] || 0) + todo.amount\n }\n for (const name in haverTotalHave) {\n haversSorted.push({ name, amount: haverTotalHave[name] })\n }\n for (const name in neederTotalReceived) {\n needersSorted.push({ name, amount: neederTotalReceived[name] })\n }\n // sort haves and needs: greatest to least\n haversSorted.sort((a, b) => b.amount - a.amount)\n needersSorted.sort((a, b) => b.amount - a.amount)\n while (haversSorted.length > 0 && needersSorted.length > 0) {\n const mostHaver = haversSorted.pop()\n const mostNeeder = needersSorted.pop()\n const diff = mostHaver.amount - mostNeeder.amount\n if (diff < 0) {\n // we used up everything the haver had\n minimizedDistribution.push({ amount: mostHaver.amount, from: mostHaver.name, to: mostNeeder.name })\n mostNeeder.amount -= mostHaver.amount\n needersSorted.push(mostNeeder)\n } else if (diff > 0) {\n // we completely filled up the needer's need and still have some left over\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n mostHaver.amount -= mostNeeder.amount\n haversSorted.push(mostHaver)\n } else {\n // a perfect match\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n }\n }\n return minimizedDistribution\n}\n", "'use strict'\n\n \n \n \n \n \n \n \n \n\n// https://github.com/okTurtles/group-income/issues/813#issuecomment-593680834\n// round all accounting to DECIMALS_MAX decimal places max to avoid consensus\n// issues that can arise due to different floating point values\n// at extreme precisions. If this becomes inadequate, instead of increasing\n// this value, switch to a different currency base, e.g. from BTC to mBTC.\nexport const DECIMALS_MAX = 8\n\nfunction commaToDots (value ) {\n // ex: \"1,55\" -> \"1.55\"\n return typeof value === 'string' ? value.replace(/,/, '.') : value.toString()\n}\n\nfunction isNumeric (nr ) {\n return !isNaN((nr ) - parseFloat(nr))\n}\n\nfunction isInDecimalsLimit (nr , decimalsMax ) {\n const decimals = nr.split('.')[1]\n return !decimals || decimals.length <= decimalsMax\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nfunction validateMincome (value , decimalsMax ) {\n const nr = commaToDots(value)\n return isNumeric(nr) && isInDecimalsLimit(nr, decimalsMax)\n}\n\nfunction decimalsOrInt (num , decimalsMax ) {\n // ex: 12.5 -> \"12.50\", but 250 -> \"250\"\n return num.toFixed(decimalsMax).replace(/\\.0+$/, '')\n}\n\nexport function saferFloat (value ) {\n // ex: 1.333333333333333333 -> 1.33333333\n return parseFloat(value.toFixed(DECIMALS_MAX))\n}\n\nexport function normalizeCurrency (value ) {\n // ex: \"1,333333333333333333\" -> 1.33333333\n return saferFloat(parseFloat(commaToDots(value)))\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nexport function mincomePositive (value ) {\n return parseFloat(commaToDots(value)) > 0\n}\n\nfunction makeCurrency (options) {\n const { symbol, symbolWithCode, decimalsMax, formatCurrency } = options\n return {\n symbol,\n symbolWithCode,\n decimalsMax,\n displayWithCurrency: (n ) => formatCurrency(decimalsOrInt(n, decimalsMax)),\n displayWithoutCurrency: (n ) => decimalsOrInt(n, decimalsMax),\n validate: (n ) => validateMincome(n, decimalsMax)\n }\n}\n\n// NOTE: if we needed for some reason, this could also be defined in\n// a json file that's read in and generates this object. For\n// example, that would allow the addition of currencies without\n// having to \"recompile\" a new version of the app.\nconst currencies = {\n USD: makeCurrency({\n symbol: '$',\n symbolWithCode: '$ USD',\n decimalsMax: 2,\n formatCurrency: amount => '$' + amount\n }),\n EUR: makeCurrency({\n symbol: '\u20AC',\n symbolWithCode: '\u20AC EUR',\n decimalsMax: 2,\n formatCurrency: amount => '\u20AC' + amount\n }),\n BTC: makeCurrency({\n symbol: '\u0243',\n symbolWithCode: '\u0243 BTC',\n decimalsMax: DECIMALS_MAX,\n formatCurrency: amount => amount + '\u0243'\n })\n}\n\nexport default currencies\n", "'use strict'\n\nimport mincomeProportional from './mincome-proportional.js'\nimport minimizeTotalPaymentsCount from './payments-minimizer.js'\nimport { cloneDeep } from '../giLodash.js'\nimport { saferFloat, DECIMALS_MAX } from '../currencies.js'\n\n \n\nconst tinyNum = 1 / Math.pow(10, DECIMALS_MAX)\n\nexport function unadjustedDistribution ({ haveNeeds = [], minimize = true } \n \n ) {\n const distribution = mincomeProportional(haveNeeds)\n return minimize ? minimizeTotalPaymentsCount(distribution) : distribution\n}\n\nexport function adjustedDistribution (\n { distribution, payments, dueOn } \n) {\n distribution = cloneDeep(distribution)\n // ensure the total is set because of how reduceDistribution works\n for (const todo of distribution) {\n todo.total = todo.amount\n }\n distribution = subtractDistributions(distribution, payments)\n // remove any todos for containing miniscule amounts\n // and pledgers who switched sides should have their todos removed\n .filter(todo => todo.amount >= tinyNum)\n for (const todo of distribution) {\n todo.amount = saferFloat(todo.amount)\n todo.total = saferFloat(todo.total)\n todo.partial = todo.total !== todo.amount\n todo.isLate = false\n todo.dueOn = dueOn\n }\n // TODO: add in latePayments to the end of the distribution\n // consider passing in latePayments\n return distribution\n}\n\n// Merges multiple payments between any combinations two of users:\nfunction reduceDistribution (payments ) {\n // Don't modify the payments list/object parameter in-place, as this is not intended:\n payments = cloneDeep(payments)\n for (let i = 0; i < payments.length; i++) {\n const paymentA = payments[i]\n for (let j = i + 1; j < payments.length; j++) {\n const paymentB = payments[j]\n\n // Were paymentA and paymentB between the same two users?\n if ((paymentA.from === paymentB.from && paymentA.to === paymentB.to) ||\n (paymentA.to === paymentB.from && paymentA.from === paymentB.to)) {\n // Add or subtract paymentB's amount to paymentA's amount, depending on the relative\n // direction of the two payments:\n paymentA.amount += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.amount\n paymentA.total += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.total\n // Remove paymentB from payments, and decrement the inner sentinal loop variable:\n payments.splice(j, 1)\n j--\n }\n }\n }\n return payments\n}\n\nfunction addDistributions (paymentsA , paymentsB ) {\n return reduceDistribution([...paymentsA, ...paymentsB])\n}\n\nfunction subtractDistributions (paymentsA , paymentsB ) {\n // Don't modify any payment list/objects parameters in-place, as this is not intended:\n paymentsB = cloneDeep(paymentsB)\n // Reverse the sign of the second operand's amounts so that the final addition is actually subtraction:\n for (const p of paymentsB) {\n p.amount *= -1\n p.total *= -1\n }\n return addDistributions(paymentsA, paymentsB)\n}\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, Errors, L } from '@common/common.js'\nimport votingRules, { ruleType, VOTE_FOR, VOTE_AGAINST, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './shared/voting/rules.js'\nimport proposals, { proposalType, proposalSettingsType, archiveProposal } from './shared/voting/proposals.js'\nimport {\n PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC, STATUS_OPEN, STATUS_CANCELLED, MAX_ARCHIVED_PROPOSALS, PROPOSAL_ARCHIVED,\n INVITE_INITIAL_CREATOR, INVITE_STATUS, PROFILE_STATUS, INVITE_EXPIRES_IN_DAYS\n} from './shared/constants.js'\nimport { paymentStatusType, paymentType, PAYMENT_COMPLETED } from './shared/payments/index.js'\nimport { merge, deepEqualJSONType, omit } from './shared/giLodash.js'\nimport { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, periodStampGivenDate, dateIsWithinPeriod, DAYS_MILLIS } from './shared/time.js'\nimport { unadjustedDistribution, adjustedDistribution } from './shared/distribution/distribution.js'\nimport currencies, { saferFloat } from './shared/currencies.js'\nimport { inviteType, chatRoomAttributesType } from './shared/types.js'\nimport { arrayOf, mapOf, objectOf, objectMaybeOf, optional, string, number, boolean, object, unionOf, tupleOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction vueFetchInitKV (obj , key , initialValue ) {\n let value = obj[key]\n if (!value) {\n Vue.set(obj, key, initialValue)\n value = obj[key]\n }\n return value\n}\n\nfunction initGroupProfile (contractID , joinedDate ) {\n return {\n globalUsername: '', // TODO: this? e.g. groupincome:greg / namecoin:bob / ens:alice\n contractID,\n joinedDate,\n nonMonetaryContributions: [],\n status: PROFILE_STATUS.ACTIVE,\n departedDate: null\n }\n}\n\nfunction initPaymentPeriod ({ getters }) {\n return {\n // this saved so that it can be used when creating a new payment\n initialCurrency: getters.groupMincomeCurrency,\n // TODO: should we also save the first period's currency exchange rate..?\n // all payments during the period use this to set their exchangeRate\n // see notes and code in groupIncomeAdjustedDistribution for details.\n // TODO: for the currency change proposal, have it update the mincomeExchangeRate\n // using .mincomeExchangeRate *= proposal.exchangeRate\n mincomeExchangeRate: 1, // modified by proposals to change mincome currency\n paymentsFrom: {}, // fromUser => toUser => Array\n // snapshot of adjusted distribution after each completed payment\n // yes, it is possible a payment began in one period and completed in another\n // in which case lastAdjustedDistribution for the previous period will be updated\n lastAdjustedDistribution: null,\n // snapshot of haveNeeds. made only when there are no payments\n haveNeedsSnapshot: null\n }\n}\n\n// NOTE: do not call any of these helper functions from within a getter b/c they modify state!\n\nfunction clearOldPayments ({ state, getters }) {\n const sortedPeriodKeys = Object.keys(state.paymentsByPeriod).sort()\n // save two periods worth of payments, max\n while (sortedPeriodKeys.length > 2) {\n const period = sortedPeriodKeys.shift()\n for (const paymentHash of getters.paymentHashesForPeriod(period)) {\n Vue.delete(state.payments, paymentHash)\n // TODO: archive the old payments in a sideEffect, not here\n }\n Vue.delete(state.paymentsByPeriod, period)\n }\n}\n\nfunction initFetchPeriodPayments ({ meta, state, getters }) {\n const period = getters.periodStampGivenDate(meta.createdDate)\n const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ getters }))\n clearOldPayments({ state, getters })\n return periodPayments\n}\n\n// this function is called each time a payment is completed or a user adjusts their income details.\n// TODO: call also when mincome is adjusted\nfunction updateCurrentDistribution ({ meta, state, getters }) {\n const curPeriodPayments = initFetchPeriodPayments({ meta, state, getters })\n const period = getters.periodStampGivenDate(meta.createdDate)\n const noPayments = Object.keys(curPeriodPayments.paymentsFrom).length === 0\n // update distributionDate if we've passed into the next period\n if (comparePeriodStamps(period, getters.groupSettings.distributionDate) > 0) {\n getters.groupSettings.distributionDate = period\n }\n // save haveNeeds if there are no payments or the haveNeeds haven't been saved yet\n if (noPayments || !curPeriodPayments.haveNeedsSnapshot) {\n curPeriodPayments.haveNeedsSnapshot = getters.haveNeedsForThisPeriod(period)\n }\n // if there are payments this period, save the adjusted distribution\n if (!noPayments) {\n updateAdjustedDistribution({ period, getters })\n }\n}\n\nfunction updateAdjustedDistribution ({ period, getters }) {\n const payments = getters.groupPeriodPayments[period]\n if (payments && payments.haveNeedsSnapshot) {\n const minimize = getters.groupSettings.minimizeDistribution\n payments.lastAdjustedDistribution = adjustedDistribution({\n distribution: unadjustedDistribution({ haveNeeds: payments.haveNeedsSnapshot, minimize }),\n payments: getters.paymentsForPeriod(period),\n dueOn: getters.dueDateForPeriod(period)\n }).filter(todo => {\n // only return todos for active members\n return getters.groupProfile(todo.to).status === PROFILE_STATUS.ACTIVE\n })\n }\n}\n\nfunction memberLeaves ({ username, dateLeft }, { meta, state, getters }) {\n state.profiles[username].status = PROFILE_STATUS.REMOVED\n state.profiles[username].departedDate = dateLeft\n // remove any todos for this member from the adjusted distribution\n updateCurrentDistribution({ meta, state, getters })\n}\n\nfunction isActionYoungerThanUser (actionMeta , userProfile ) {\n // A util function that checks if an action (or event) in a group occurred after a particular user joined a group.\n // This is used mostly for checking if a notification should be sent for that user or not.\n // e.g.) user-2 who joined a group later than user-1 (who is the creator of the group) doesn't need to receive\n // 'MEMBER_ADDED' notification for user-1.\n // In some situations, userProfile is undefined, for example, when inviteAccept is called in\n // certain situations. So we need to check for that here.\n return Boolean(userProfile) && compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/group',\n metadata: {\n validate: objectOf({\n createdDate: string,\n username: string,\n identityContractID: string\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n // TODO: We may want to get the time from the server instead of relying on\n // the client in case the client's clock isn't set correctly.\n // the only issue here is that it involves an async function...\n // See: https://github.com/okTurtles/group-income/issues/531\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n // These getters are restricted only to the contract's state.\n // Do not access state outside the contract state inside of them.\n // For example, if the getter you use tries to access `state.loggedIn`,\n // that will break the `latestContractState` function in state.js.\n // It is only safe to access state outside of the contract in a contract action's\n // `sideEffect` function (as long as it doesn't modify contract state)\n getters: {\n // we define `currentGroupState` here so that we can redefine it in state.js\n // so that we can re-use these getter definitions in state.js since they are\n // compatible with Vuex getter definitions.\n // Here `state` refers to the individual group contract's state, the equivalent\n // of `vuexRootState[someGroupContractID]`.\n currentGroupState (state) {\n return state\n },\n groupSettings (state, getters) {\n return getters.currentGroupState.settings || {}\n },\n groupProfile (state, getters) {\n return username => {\n const profiles = getters.currentGroupState.profiles\n return profiles && profiles[username]\n }\n },\n groupProfiles (state, getters) {\n const profiles = {}\n for (const username in (getters.currentGroupState.profiles || {})) {\n const profile = getters.groupProfile(username)\n if (profile.status === PROFILE_STATUS.ACTIVE) {\n profiles[username] = profile\n }\n }\n return profiles\n },\n groupMincomeAmount (state, getters) {\n return getters.groupSettings.mincomeAmount\n },\n groupMincomeCurrency (state, getters) {\n return getters.groupSettings.mincomeCurrency\n },\n periodStampGivenDate (state, getters) {\n return (recentDate ) => {\n if (typeof recentDate !== 'string') {\n recentDate = recentDate.toISOString()\n }\n const { distributionDate, distributionPeriodLength } = getters.groupSettings\n return periodStampGivenDate({\n recentDate,\n periodStart: distributionDate,\n periodLength: distributionPeriodLength\n })\n }\n },\n periodBeforePeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -len))\n }\n },\n periodAfterPeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), len))\n }\n },\n dueDateForPeriod (state, getters) {\n return (periodStamp ) => {\n return dateToPeriodStamp(\n addTimeToDate(\n dateFromPeriodStamp(getters.periodAfterPeriod(periodStamp)),\n -DAYS_MILLIS\n )\n )\n }\n },\n paymentTotalFromUserToUser (state, getters) {\n return (fromUser, toUser, periodStamp) => {\n const payments = getters.currentGroupState.payments\n const periodPayments = getters.groupPeriodPayments\n const { paymentsFrom, mincomeExchangeRate } = periodPayments[periodStamp] || {}\n // NOTE: @babel/plugin-proposal-optional-chaining would come in super-handy\n // here, but I couldn't get it to work with our linter. :(\n // https://github.com/babel/babel-eslint/issues/511\n const total = (((paymentsFrom || {})[fromUser] || {})[toUser] || []).reduce((a, hash) => {\n const payment = payments[hash]\n let { amount, exchangeRate, status } = payment.data\n if (status !== PAYMENT_COMPLETED) {\n return a\n }\n const paymentCreatedPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n // if this payment is from a previous period, then make sure to take into account\n // any proposals that passed in between the payment creation and the payment\n // completion that modified the group currency by multiplying both period's\n // exchange rates\n if (periodStamp !== paymentCreatedPeriodStamp) {\n if (paymentCreatedPeriodStamp !== getters.periodBeforePeriod(periodStamp)) {\n console.warn(`paymentTotalFromUserToUser: super old payment shouldn't exist, ignoring! (curPeriod=${periodStamp})`, JSON.stringify(payment))\n return a\n }\n exchangeRate *= periodPayments[paymentCreatedPeriodStamp].mincomeExchangeRate\n }\n return a + (amount * exchangeRate * mincomeExchangeRate)\n }, 0)\n return saferFloat(total)\n }\n },\n paymentHashesForPeriod (state, getters) {\n return (periodStamp) => {\n const periodPayments = getters.groupPeriodPayments[periodStamp]\n if (periodPayments) {\n let hashes = []\n const { paymentsFrom } = periodPayments\n for (const fromUser in paymentsFrom) {\n for (const toUser in paymentsFrom[fromUser]) {\n hashes = hashes.concat(paymentsFrom[fromUser][toUser])\n }\n }\n return hashes\n }\n }\n },\n groupMembersByUsername (state, getters) {\n return Object.keys(getters.groupProfiles)\n },\n groupMembersCount (state, getters) {\n return getters.groupMembersByUsername.length\n },\n groupMembersPending (state, getters) {\n const invites = getters.currentGroupState.invites\n const pendingMembers = {}\n for (const inviteId in invites) {\n const invite = invites[inviteId]\n if (\n invite.status === INVITE_STATUS.VALID &&\n invite.creator !== INVITE_INITIAL_CREATOR\n ) {\n pendingMembers[invites[inviteId].invitee] = {\n invitedBy: invites[inviteId].creator,\n expires: invite.expires\n }\n }\n }\n return pendingMembers\n },\n groupShouldPropose (state, getters) {\n return getters.groupMembersCount >= 3\n },\n groupProposalSettings (state, getters) {\n return (proposalType = PROPOSAL_GENERIC) => {\n return getters.groupSettings.proposals[proposalType]\n }\n },\n groupCurrency (state, getters) {\n const mincomeCurrency = getters.groupMincomeCurrency\n return mincomeCurrency && currencies[mincomeCurrency]\n },\n groupMincomeFormatted (state, getters) {\n return getters.withGroupCurrency?.(getters.groupMincomeAmount)\n },\n groupMincomeSymbolWithCode (state, getters) {\n return getters.groupCurrency?.symbolWithCode\n },\n groupPeriodPayments (state, getters) {\n // note: a lot of code expects this to return an object, so keep the || {} below\n return getters.currentGroupState.paymentsByPeriod || {}\n },\n withGroupCurrency (state, getters) {\n // TODO: If this group has no defined mincome currency, not even a default one like\n // USD, then calling this function is probably an error which should be reported.\n // Just make sure the UI doesn't break if an exception is thrown, since this is\n // bound to the UI in some location.\n return getters.groupCurrency?.displayWithCurrency\n },\n getChatRooms (state, getters) {\n return getters.currentGroupState.chatRooms\n },\n generalChatRoomId (state, getters) {\n return getters.currentGroupState.generalChatRoomId\n },\n // getter is named haveNeedsForThisPeriod instead of haveNeedsForPeriod because it uses\n // getters.groupProfiles - and that is always based on the most recent values. we still\n // pass in the current period because it's used to set the \"when\" property\n haveNeedsForThisPeriod (state, getters) {\n return (currentPeriod ) => {\n // NOTE: if we ever switch back to the \"real-time\" adjusted distribution algorithm,\n // make sure that this function also handles userExitsGroupEvent\n const groupProfiles = getters.groupProfiles // TODO: these should use the haveNeeds for the specific period's distribution period\n const haveNeeds = []\n for (const username in groupProfiles) {\n const { incomeDetailsType, joinedDate } = groupProfiles[username]\n if (incomeDetailsType) {\n const amount = groupProfiles[username][incomeDetailsType]\n const haveNeed = incomeDetailsType === 'incomeAmount' ? amount - getters.groupMincomeAmount : amount\n // construct 'when' this way in case we ever use a pro-rated algorithm\n let when = dateFromPeriodStamp(currentPeriod).toISOString()\n if (dateIsWithinPeriod({\n date: joinedDate,\n periodStart: currentPeriod,\n periodLength: getters.groupSettings.distributionPeriodLength\n })) {\n when = joinedDate\n }\n haveNeeds.push({ name: username, haveNeed, when })\n }\n }\n return haveNeeds\n }\n },\n paymentsForPeriod (state, getters) {\n return (periodStamp) => {\n const hashes = getters.paymentHashesForPeriod(periodStamp)\n const events = []\n if (hashes && hashes.length > 0) {\n const payments = getters.currentGroupState.payments\n for (const paymentHash of hashes) {\n const payment = payments[paymentHash]\n if (payment.data.status === PAYMENT_COMPLETED) {\n events.push({\n from: payment.meta.username,\n to: payment.data.toUser,\n hash: paymentHash,\n amount: payment.data.amount,\n isLate: !!payment.data.isLate,\n when: payment.data.completedDate\n })\n }\n }\n }\n return events\n }\n }\n // distributionEventsForMonth (state, getters) {\n // return (monthstamp) => {\n // // NOTE: if we ever switch back to the \"real-time\" adjusted distribution\n // // algorithm, make sure that this function also handles userExitsGroupEvent\n // const distributionEvents = getters.haveNeedEventsForMonth(monthstamp)\n // const paymentEvents = getters.paymentEventsForMonth(monthstamp)\n // distributionEvents.splice(distributionEvents.length, 0, paymentEvents)\n // return distributionEvents.sort((a, b) => compareISOTimestamps(a.data.when, b.data.when))\n // }\n // }\n },\n // NOTE: All mutations must be atomic in their edits of the contract state.\n // THEY ARE NOT to farm out any further mutations through the async actions!\n actions: {\n // this is the constructor\n 'gi.contracts/group': {\n validate: objectMaybeOf({\n invites: mapOf(string, inviteType),\n settings: objectMaybeOf({\n // TODO: add 'groupPubkey'\n groupName: string,\n groupPicture: string,\n sharedValues: string,\n mincomeAmount: number,\n mincomeCurrency: string,\n distributionDate: isPeriodStamp,\n distributionPeriodLength: number,\n minimizeDistribution: boolean,\n proposals: objectOf({\n [PROPOSAL_INVITE_MEMBER]: proposalSettingsType,\n [PROPOSAL_REMOVE_MEMBER]: proposalSettingsType,\n [PROPOSAL_GROUP_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_GENERIC]: proposalSettingsType\n })\n })\n }),\n process ({ data, meta }, { state, getters }) {\n // TODO: checkpointing: https://github.com/okTurtles/group-income/issues/354\n const initialState = merge({\n payments: {},\n paymentsByPeriod: {},\n invites: {},\n proposals: {}, // hashes => {} TODO: this, see related TODOs in GroupProposal\n settings: {\n groupCreator: meta.username,\n distributionPeriodLength: 30 * DAYS_MILLIS,\n inviteExpiryOnboarding: INVITE_EXPIRES_IN_DAYS.ON_BOARDING,\n inviteExpiryProposal: INVITE_EXPIRES_IN_DAYS.PROPOSAL\n },\n profiles: {\n [meta.username]: initGroupProfile(meta.identityContractID, meta.createdDate)\n },\n chatRooms: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n initFetchPeriodPayments({ meta, state, getters })\n }\n },\n 'gi.contracts/group/payment': {\n validate: objectMaybeOf({\n // TODO: how to handle donations to okTurtles?\n // TODO: how to handle payments to groups or users outside of this group?\n toUser: string,\n amount: number,\n currencyFromTo: tupleOf(string, string), // must be one of the keys in currencies.js (e.g. USD, EUR, etc.) TODO: handle old clients not having one of these keys, see OP_PROTOCOL_UPGRADE https://github.com/okTurtles/group-income/issues/603\n // multiply 'amount' by 'exchangeRate', which must always be\n // based on the initialCurrency of the period in which this payment was created.\n // it is then further multiplied by the period's 'mincomeExchangeRate', which\n // is modified if any proposals pass to change the mincomeCurrency\n exchangeRate: number,\n txid: string,\n status: paymentStatusType,\n paymentType: paymentType,\n details: optional(object),\n memo: optional(string)\n }),\n process ({ data, meta, hash }, { state, getters }) {\n if (data.status === PAYMENT_COMPLETED) {\n console.error(`payment: payment ${hash} cannot have status = 'completed'!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('payments cannot be instantly completed!')\n }\n Vue.set(state.payments, hash, {\n data: {\n ...data,\n groupMincome: getters.groupMincomeAmount\n },\n meta,\n history: [[meta.createdDate, hash]]\n })\n const { paymentsFrom } = initFetchPeriodPayments({ meta, state, getters })\n const fromUser = vueFetchInitKV(paymentsFrom, meta.username, {})\n const toUser = vueFetchInitKV(fromUser, data.toUser, [])\n toUser.push(hash)\n // TODO: handle completed payments here too! (for manual payment support)\n }\n },\n 'gi.contracts/group/paymentUpdate': {\n validate: objectMaybeOf({\n paymentHash: string,\n updatedProperties: objectMaybeOf({\n status: paymentStatusType,\n details: object,\n memo: string\n })\n }),\n process ({ data, meta, hash }, { state, getters }) {\n // TODO: we don't want to keep a history of all payments in memory all the time\n // https://github.com/okTurtles/group-income/issues/426\n const payment = state.payments[data.paymentHash]\n // TODO: move these types of validation errors into the validate function so\n // that they can be done before sending as well as upon receiving\n if (!payment) {\n console.error(`paymentUpdate: no payment ${data.paymentHash}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate without existing payment')\n }\n // if the payment is being modified by someone other than the person who sent or received it, throw an exception\n if (meta.username !== payment.meta.username && meta.username !== payment.data.toUser) {\n console.error(`paymentUpdate: bad username ${meta.username} != ${payment.meta.username} != ${payment.data.username}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate from bad user!')\n }\n payment.history.push([meta.createdDate, hash])\n merge(payment.data, data.updatedProperties)\n // we update \"this period\"'s snapshot 'lastAdjustedDistribution' on each completed payment\n if (data.updatedProperties.status === PAYMENT_COMPLETED) {\n payment.data.completedDate = meta.createdDate\n // update the current distribution unless this update is for a payment from the previous period\n const updatePeriodStamp = getters.periodStampGivenDate(meta.createdDate)\n const paymentPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n if (comparePeriodStamps(updatePeriodStamp, paymentPeriodStamp) > 0) {\n updateAdjustedDistribution({ period: paymentPeriodStamp, getters })\n } else {\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n }\n },\n 'gi.contracts/group/proposal': {\n validate: (data, { state, meta }) => {\n objectOf({\n proposalType: proposalType,\n proposalData: object, // data for Vue widgets\n votingRule: ruleType,\n expires_date_ms: number // calculate by grabbing proposal expiry from group properties and add to `meta.createdDate`\n })(data)\n\n const dataToCompare = omit(data.proposalData, ['reason'])\n\n // Validate this isn't a duplicate proposal\n for (const hash in state.proposals) {\n const prop = state.proposals[hash]\n if (prop.status !== STATUS_OPEN || prop.data.proposalType !== data.proposalType) {\n continue\n }\n\n if (deepEqualJSONType(omit(prop.data.proposalData, ['reason']), dataToCompare)) {\n throw new TypeError(L('There is an identical open proposal.'))\n }\n\n // TODO - verify if type of proposal already exists (SETTING_CHANGE).\n }\n },\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.proposals, hash, {\n data,\n meta,\n votes: { [meta.username]: VOTE_FOR },\n status: STATUS_OPEN,\n payload: null // set later by group/proposalVote\n })\n // TODO: save all proposals disk so that we only keep open proposals in memory\n // TODO: create a global timer to auto-pass/archive expired votes\n // make sure to set that proposal's status as STATUS_EXPIRED if it's expired\n },\n sideEffect ({ contractID, meta, data }, { getters }) {\n const { loggedIn } = sbp('state/vuex/state')\n const typeToSubTypeMap = {\n [PROPOSAL_INVITE_MEMBER]: 'ADD_MEMBER',\n [PROPOSAL_REMOVE_MEMBER]: 'REMOVE_MEMBER',\n [PROPOSAL_GROUP_SETTING_CHANGE]: 'CHANGE_MINCOME',\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: 'CHANGE_VOTING_RULE',\n [PROPOSAL_GENERIC]: 'GENERIC'\n }\n\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'NEW_PROPOSAL', {\n groupID: contractID,\n creator: meta.username,\n subtype: typeToSubTypeMap[data.proposalType]\n })\n }\n }\n },\n 'gi.contracts/group/proposalVote': {\n validate: objectOf({\n proposalHash: string,\n vote: string,\n passPayload: optional(unionOf(object, string)) // TODO: this, somehow we need to send an OP_KEY_ADD GIMessage to add a generated once-only writeonly message public key to the contract, and (encrypted) include the corresponding invite link, also, we need all clients to verify that this message/operation was valid to prevent a hacked client from adding arbitrary OP_KEY_ADD messages, and automatically ban anyone generating such messages\n }),\n process (message, { state }) {\n const { data, hash, meta } = message\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalVote: no proposal for ${data.proposalHash}!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n }\n Vue.set(proposal.votes, meta.username, data.vote)\n // TODO: handle vote pass/fail\n // check if proposal is expired, if so, ignore (but log vote)\n if (new Date(meta.createdDate).getTime() > proposal.data.expires_date_ms) {\n console.warn('proposalVote: vote on expired proposal!', { proposal, data, meta })\n // TODO: display warning or something\n return\n }\n // see if this is a deciding vote\n const result = votingRules[proposal.data.votingRule](state, proposal.data.proposalType, proposal.votes)\n if (result === VOTE_FOR || result === VOTE_AGAINST) {\n // handles proposal pass or fail, will update proposal.status accordingly\n proposals[proposal.data.proposalType][result](state, message)\n Vue.set(proposal, 'dateClosed', meta.createdDate)\n }\n },\n sideEffect ({ contractID, data, meta }, { state, getters }) {\n const proposal = state.proposals[data.proposalHash]\n const { loggedIn } = sbp('state/vuex/state')\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (proposal?.dateClosed &&\n isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'PROPOSAL_CLOSED', {\n groupID: contractID,\n creator: meta.username,\n proposalStatus: proposal.status\n })\n }\n }\n },\n 'gi.contracts/group/proposalCancel': {\n validate: objectOf({\n proposalHash: string\n }),\n process ({ data, meta, contractID }, { state }) {\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalCancel: no proposal for ${data.proposalHash}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n } else if (proposal.meta.username !== meta.username) {\n console.error(`proposalCancel: proposal ${data.proposalHash} belongs to ${proposal.meta.username} not ${meta.username}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalWithdraw for wrong user!')\n }\n Vue.set(proposal, 'status', STATUS_CANCELLED)\n archiveProposal({ state, proposalHash: data.proposalHash, proposal, contractID })\n }\n },\n 'gi.contracts/group/removeMember': {\n validate: (data, { state, getters, meta }) => {\n objectOf({\n member: string, // username to remove\n reason: optional(string),\n // In case it happens in a big group (by proposal)\n // we need to validate the associated proposal.\n proposalHash: optional(string),\n proposalPayload: optional(objectOf({\n secret: string // NOTE: simulate the OP_KEY_* stuff for now\n }))\n })(data)\n\n const memberToRemove = data.member\n const membersCount = getters.groupMembersCount\n\n if (!state.profiles[memberToRemove]) {\n throw new TypeError(L('Not part of the group.'))\n }\n if (membersCount === 1 || memberToRemove === meta.username) {\n throw new TypeError(L('Cannot remove yourself.'))\n }\n\n if (membersCount < 3) {\n // In a small group only the creator can remove someone\n // TODO: check whether meta.username has required admin permissions\n if (meta.username !== state.settings.groupCreator) {\n throw new TypeError(L('Only the group creator can remove members.'))\n }\n } else {\n // In a big group a removal can only happen through a proposal\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // TODO this\n throw new TypeError(L('Admin credentials needed and not implemented yet.'))\n }\n\n if (!proposal.payload || proposal.payload.secret !== data.proposalPayload.secret) {\n throw new TypeError(L('Invalid associated proposal.'))\n }\n }\n },\n process ({ data, meta }, { state, getters }) {\n memberLeaves(\n { username: data.member, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n },\n sideEffect ({ data, meta, contractID }, { state, getters }) {\n const rootState = sbp('state/vuex/state')\n const contracts = rootState.contracts || {}\n const { username } = rootState.loggedIn\n\n if (data.member === username) {\n // If this member is re-joining the group, ignore the rest\n // so the member doesn't remove themself again.\n if (sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n return\n }\n\n const groupIdToSwitch = Object.keys(contracts)\n .find(cID => contracts[cID].type === 'gi.contracts/group' &&\n cID !== contractID && rootState[cID].settings) || null\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {})\n sbp('state/vuex/commit', 'setCurrentGroupId', groupIdToSwitch)\n // we can't await on this in here, because it will cause a deadlock, since Chelonia processes\n // this sideEffect on the eventqueue for this contractID, and /remove uses that same eventqueue\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown by /remove ${contractID}:`, e)\n })\n // this looks crazy, but doing this was necessary to fix a race condition in the\n // group-member-removal Cypress tests where due to the ordering of asynchronous events\n // we were getting the same latestHash upon re-logging in for test \"user2 rejoins groupA\".\n // We add it to the same queue as '/remove' above gets run on so that it is run after\n // contractID is removed. See also comments in 'gi.actions/identity/login'.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/saveOurLoginState'])\n .then(function () {\n const router = sbp('controller/router')\n const switchFrom = router.currentRoute.path\n const switchTo = groupIdToSwitch ? '/dashboard' : '/'\n if (switchFrom !== '/join' && switchFrom !== switchTo) {\n router.push({ path: switchTo }).catch(console.warn)\n }\n }).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown during queueEvent to ${contractID} by saveOurLoginState:`, e)\n })\n // TODO - #828 remove other group members contracts if applicable\n } else {\n const myProfile = getters.groupProfile(username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n const memberRemovedThemselves = data.member === meta.username\n\n sbp('gi.notifications/emit', // emit a notification for a member removal.\n memberRemovedThemselves ? 'MEMBER_LEFT' : 'MEMBER_REMOVED',\n {\n groupID: contractID,\n username: memberRemovedThemselves ? meta.username : data.member\n })\n }\n // TODO - #828 remove the member contract if applicable.\n // problem is, if they're in another group we're also a part of, or if we\n // have a DM with them, we don't want to do this. may need to use manual reference counting\n // sbp('chelonia/contract/release', getters.groupProfile(data.member).contractID)\n }\n // TODO - #850 verify open proposals and see if they need some re-adjustment.\n }\n },\n 'gi.contracts/group/removeOurselves': {\n validate: objectMaybeOf({\n reason: string\n }),\n process ({ data, meta, contractID }, { state, getters }) {\n memberLeaves(\n { username: meta.username, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', {\n meta,\n data: { member: meta.username, reason: data.reason || '' },\n contractID\n }]\n )\n }\n },\n 'gi.contracts/group/invite': {\n validate: inviteType,\n process ({ data, meta }, { state }) {\n Vue.set(state.invites, data.inviteSecret, data)\n }\n },\n 'gi.contracts/group/inviteAccept': {\n validate: objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n }),\n process ({ data, meta }, { state }) {\n console.debug('inviteAccept:', data, state.invites)\n const invite = state.invites[data.inviteSecret]\n if (invite.status !== INVITE_STATUS.VALID) {\n console.error(`inviteAccept: invite for ${meta.username} is: ${invite.status}`)\n return\n }\n Vue.set(invite.responses, meta.username, true)\n if (Object.keys(invite.responses).length === invite.quantity) {\n invite.status = INVITE_STATUS.USED\n }\n // TODO: ensure `meta.username` is unique for the lifetime of the username\n // since we are making it possible for the same username to leave and\n // rejoin the group. All of their past posts will be re-associated with\n // them upon re-joining.\n Vue.set(state.profiles, meta.username, initGroupProfile(meta.identityContractID, meta.createdDate))\n // If we're triggered by handleEvent in state.js (and not latestContractState)\n // then the asynchronous sideEffect function will get called next\n // and we will subscribe to this new user's identity contract\n },\n // !! IMPORANT!!\n // Actions here MUST NOT modify contract state!\n // They MUST NOT call 'commit'!\n // They should only coordinate the actions of outside contracts.\n // Otherwise `latestContractState` and `handleEvent` will not produce same state!\n async sideEffect ({ meta, contractID }, { state }) {\n const { loggedIn } = sbp('state/vuex/state')\n const { profiles = {} } = state\n\n // TODO: per #257 this will ,have to be encompassed in a recoverable transaction\n // however per #610 that might be handled in handleEvent (?), or per #356 might not be needed\n if (meta.username === loggedIn.username) {\n // we're the person who just accepted the group invite\n // so subscribe to founder's IdentityContract & everyone else's\n for (const name in profiles) {\n if (name !== loggedIn.username) {\n await sbp('chelonia/contract/sync', profiles[name].contractID)\n }\n }\n } else {\n const myProfile = profiles[loggedIn.username]\n // we're an existing member of the group getting notified that a\n // new member has joined, so subscribe to their identity contract\n await sbp('chelonia/contract/sync', meta.identityContractID)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'MEMBER_ADDED', { // emit a notification for a member addition.\n groupID: contractID,\n username: meta.username\n })\n }\n }\n }\n },\n 'gi.contracts/group/inviteRevoke': {\n validate: (data, { state, meta }) => {\n objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n })(data)\n\n if (!state.invites[data.inviteSecret]) {\n throw new TypeError(L('The link does not exist.'))\n }\n },\n process ({ data, meta }, { state }) {\n const invite = state.invites[data.inviteSecret]\n Vue.set(invite, 'status', INVITE_STATUS.REVOKED)\n }\n },\n 'gi.contracts/group/updateSettings': {\n // OPTIMIZE: Make this custom validation function\n // reusable accross other future validators\n validate: objectMaybeOf({\n groupName: x => typeof x === 'string',\n groupPicture: x => typeof x === 'string',\n sharedValues: x => typeof x === 'string',\n mincomeAmount: x => typeof x === 'number' && x > 0,\n mincomeCurrency: x => typeof x === 'string'\n }),\n process ({ meta, data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/group/groupProfileUpdate': {\n validate: objectMaybeOf({\n incomeDetailsType: x => ['incomeAmount', 'pledgeAmount'].includes(x),\n incomeAmount: x => typeof x === 'number' && x >= 0,\n pledgeAmount: x => typeof x === 'number' && x >= 0,\n nonMonetaryAdd: string,\n nonMonetaryEdit: objectOf({\n replace: string,\n with: string\n }),\n nonMonetaryRemove: string,\n paymentMethods: arrayOf(\n objectOf({\n name: string,\n value: string\n })\n )\n }),\n process ({ data, meta }, { state, getters }) {\n const groupProfile = state.profiles[meta.username]\n const nonMonetary = groupProfile.nonMonetaryContributions\n for (const key in data) {\n const value = data[key]\n switch (key) {\n case 'nonMonetaryAdd':\n nonMonetary.push(value)\n break\n case 'nonMonetaryRemove':\n nonMonetary.splice(nonMonetary.indexOf(value), 1)\n break\n case 'nonMonetaryEdit':\n nonMonetary.splice(nonMonetary.indexOf(value.replace), 1, value.with)\n break\n default:\n Vue.set(groupProfile, key, value)\n }\n }\n if (data.incomeDetailsType) {\n // someone updated their income details, create a snapshot of the haveNeeds\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n },\n 'gi.contracts/group/updateAllVotingRules': {\n validate: objectMaybeOf({\n ruleName: x => [RULE_PERCENTAGE, RULE_DISAGREEMENT].includes(x),\n ruleThreshold: number,\n expires_ms: number\n }),\n process ({ data, meta }, { state }) {\n // Update all types of proposal settings for simplicity\n if (data.ruleName && data.ruleThreshold) {\n for (const proposalSettings in state.settings.proposals) {\n Vue.set(state.settings.proposals[proposalSettings], 'rule', data.ruleName)\n Vue.set(state.settings.proposals[proposalSettings].ruleSettings[data.ruleName], 'threshold', data.ruleThreshold)\n }\n }\n\n // TODO later - support update expires_ms\n // if (data.ruleName && data.expires_ms) {\n // for (const proposalSetting in state.settings.proposals) {\n // Vue.set(state.settings.proposals[proposalSetting].ruleSettings[data.ruleName], 'expires_ms', data.expires_ms)\n // }\n // }\n }\n },\n 'gi.contracts/group/addChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n attributes: chatRoomAttributesType\n }),\n process ({ data, meta }, { state }) {\n const { name, type, privacyLevel } = data.attributes\n Vue.set(state.chatRooms, data.chatRoomID, {\n creator: meta.username,\n name,\n type,\n privacyLevel,\n deletedDate: null,\n users: []\n })\n if (!state.generalChatRoomId) {\n Vue.set(state, 'generalChatRoomId', data.chatRoomID)\n }\n }\n },\n 'gi.contracts/group/deleteChatRoom': {\n validate: (data, { getters, meta }) => {\n objectOf({ chatRoomID: string })(data)\n\n if (getters.getChatRooms[data.chatRoomID].creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state }) {\n Vue.delete(state.chatRooms, data.chatRoomID)\n }\n },\n 'gi.contracts/group/leaveChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n member: string,\n leavingGroup: boolean // if kicker is exists, it means group leaving\n }),\n process ({ data, meta }, { state }) {\n Vue.set(state.chatRooms[data.chatRoomID], 'users',\n state.chatRooms[data.chatRoomID].users.filter(u => u !== data.member))\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (meta.username === rootState.loggedIn.username && !sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n const sendingData = data.leavingGroup\n ? { member: data.member }\n : { member: data.member, username: meta.username }\n await sbp('gi.actions/chatroom/leave', { contractID: data.chatRoomID, data: sendingData })\n }\n }\n },\n 'gi.contracts/group/joinChatRoom': {\n validate: objectMaybeOf({\n username: string,\n chatRoomID: string\n }),\n process ({ data, meta }, { state }) {\n const username = data.username || meta.username\n state.chatRooms[data.chatRoomID].users.push(username)\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n const username = data.username || meta.username\n if (username === rootState.loggedIn.username) {\n if (!sbp('okTurtles.data/get', 'JOINING_GROUP') || sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n // while users are joining chatroom, they don't need to leave chatrooms\n // this is similar to setting 'JOINING_GROUP' before joining group\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', data.chatRoomID)\n await sbp('chelonia/contract/sync', data.chatRoomID)\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', undefined)\n sbp('okTurtles.data/set', 'READY_TO_JOIN_CHATROOM', false)\n }\n }\n }\n },\n 'gi.contracts/group/renameChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n name: string\n }),\n process ({ data, meta }, { state, getters }) {\n Vue.set(state.chatRooms, data.chatRoomID, {\n ...getters.getChatRooms[data.chatRoomID],\n name: data.name\n })\n }\n },\n ...((process.env.NODE_ENV === 'development' || process.env.CI) && {\n 'gi.contracts/group/forceDistributionDate': {\n validate: optional,\n process ({ meta }, { state, getters }) {\n getters.groupSettings.distributionDate = dateToPeriodStamp(meta.createdDate)\n }\n },\n 'gi.contracts/group/malformedMutation': {\n validate: objectOf({ errorType: string, sideEffect: optional(boolean) }),\n process ({ data }) {\n const ErrorType = Errors[data.errorType]\n if (data.sideEffect) return\n if (ErrorType) {\n throw new ErrorType('malformedMutation!')\n } else {\n throw new Error(`unknown error type: ${data.errorType}`)\n }\n },\n sideEffect (message, { state }) {\n if (!message.data.sideEffect) return\n sbp('gi.contracts/group/malformedMutation/process', {\n ...message,\n data: omit(message.data, ['sideEffect'])\n }, state)\n }\n }\n })\n // TODO: remove group profile when leave group is implemented\n },\n // methods are SBP selectors that are version-tracked for each contract.\n // in other words, you can use them to define SBP selectors that will\n // contain functions that you can modify across different contract versions,\n // and when the contract calls them, it will use that specific version of the\n // method.\n //\n // They are useful when used in conjunction with pushSideEffect from process\n // functions.\n //\n // IMPORTANT: they MUST begin with the name of the contract.\n methods: {\n 'gi.contracts/group/archiveProposal': async function (contractID, proposalHash, proposal) {\n const { username } = sbp('state/vuex/state').loggedIn\n const key = `proposals/${username}/${contractID}`\n const proposals = await sbp('gi.db/archive/load', key) || []\n // newest at the front of the array, oldest at the back\n proposals.unshift([proposalHash, proposal])\n while (proposals.length > MAX_ARCHIVED_PROPOSALS) {\n proposals.pop()\n }\n await sbp('gi.db/archive/save', key, proposals)\n sbp('okTurtles.events/emit', PROPOSAL_ARCHIVED, [proposalHash, proposal])\n }\n }\n})\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nexport class GIErrorIgnoreAndBan extends Error {\n // ugly boilerplate because JavaScript is stupid\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types\n constructor (...params ) {\n super(...params)\n this.name = 'GIErrorIgnoreAndBan'\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n\n// Used to throw human readable errors on UI.\nexport class GIErrorUIRuntimeError extends Error {\n constructor (...params ) {\n super(...params)\n // this.name = this.constructor.name\n this.name = 'GIErrorUIRuntimeError' // string literal so minifier doesn't overwrite\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n", "'use strict'\n\n \n \n \n \n\nexport default function mincomeProportional (haveNeeds ) {\n let totalHave = 0\n let totalNeed = 0\n const havers = []\n const needers = []\n for (const haveNeed of haveNeeds) {\n if (haveNeed.haveNeed > 0) {\n havers.push(haveNeed)\n totalHave += haveNeed.haveNeed\n } else if (haveNeed.haveNeed < 0) {\n needers.push(haveNeed)\n totalNeed += Math.abs(haveNeed.haveNeed)\n }\n }\n const totalPercent = Math.min(1, totalNeed / totalHave)\n const payments = []\n for (const haver of havers) {\n const distributionAmount = totalPercent * haver.haveNeed\n for (const needer of needers) {\n const belowPercentage = Math.abs(needer.haveNeed) / totalNeed\n payments.push({\n amount: distributionAmount * belowPercentage,\n from: haver.name,\n to: needer.name\n })\n }\n }\n return payments\n}\n", "'use strict'\n\n// greedy algorithm responsible for \"balancing\" payments\n// such that the least number of payments are made.\nexport default function minimizeTotalPaymentsCount (\n distribution \n) {\n const neederTotalReceived = {}\n const haverTotalHave = {}\n const haversSorted = []\n const needersSorted = []\n const minimizedDistribution = []\n for (const todo of distribution) {\n neederTotalReceived[todo.to] = (neederTotalReceived[todo.to] || 0) + todo.amount\n haverTotalHave[todo.from] = (haverTotalHave[todo.from] || 0) + todo.amount\n }\n for (const name in haverTotalHave) {\n haversSorted.push({ name, amount: haverTotalHave[name] })\n }\n for (const name in neederTotalReceived) {\n needersSorted.push({ name, amount: neederTotalReceived[name] })\n }\n // sort haves and needs: greatest to least\n haversSorted.sort((a, b) => b.amount - a.amount)\n needersSorted.sort((a, b) => b.amount - a.amount)\n while (haversSorted.length > 0 && needersSorted.length > 0) {\n const mostHaver = haversSorted.pop()\n const mostNeeder = needersSorted.pop()\n const diff = mostHaver.amount - mostNeeder.amount\n if (diff < 0) {\n // we used up everything the haver had\n minimizedDistribution.push({ amount: mostHaver.amount, from: mostHaver.name, to: mostNeeder.name })\n mostNeeder.amount -= mostHaver.amount\n needersSorted.push(mostNeeder)\n } else if (diff > 0) {\n // we completely filled up the needer's need and still have some left over\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n mostHaver.amount -= mostNeeder.amount\n haversSorted.push(mostHaver)\n } else {\n // a perfect match\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n }\n }\n return minimizedDistribution\n}\n", "'use strict'\n\n \n \n \n \n \n \n \n \n\n// https://github.com/okTurtles/group-income/issues/813#issuecomment-593680834\n// round all accounting to DECIMALS_MAX decimal places max to avoid consensus\n// issues that can arise due to different floating point values\n// at extreme precisions. If this becomes inadequate, instead of increasing\n// this value, switch to a different currency base, e.g. from BTC to mBTC.\nexport const DECIMALS_MAX = 8\n\nfunction commaToDots (value ) {\n // ex: \"1,55\" -> \"1.55\"\n return typeof value === 'string' ? value.replace(/,/, '.') : value.toString()\n}\n\nfunction isNumeric (nr ) {\n return !isNaN((nr ) - parseFloat(nr))\n}\n\nfunction isInDecimalsLimit (nr , decimalsMax ) {\n const decimals = nr.split('.')[1]\n return !decimals || decimals.length <= decimalsMax\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nfunction validateMincome (value , decimalsMax ) {\n const nr = commaToDots(value)\n return isNumeric(nr) && isInDecimalsLimit(nr, decimalsMax)\n}\n\nfunction decimalsOrInt (num , decimalsMax ) {\n // ex: 12.5 -> \"12.50\", but 250 -> \"250\"\n return num.toFixed(decimalsMax).replace(/\\.0+$/, '')\n}\n\nexport function saferFloat (value ) {\n // ex: 1.333333333333333333 -> 1.33333333\n return parseFloat(value.toFixed(DECIMALS_MAX))\n}\n\nexport function normalizeCurrency (value ) {\n // ex: \"1,333333333333333333\" -> 1.33333333\n return saferFloat(parseFloat(commaToDots(value)))\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nexport function mincomePositive (value ) {\n return parseFloat(commaToDots(value)) > 0\n}\n\nfunction makeCurrency (options) {\n const { symbol, symbolWithCode, decimalsMax, formatCurrency } = options\n return {\n symbol,\n symbolWithCode,\n decimalsMax,\n displayWithCurrency: (n ) => formatCurrency(decimalsOrInt(n, decimalsMax)),\n displayWithoutCurrency: (n ) => decimalsOrInt(n, decimalsMax),\n validate: (n ) => validateMincome(n, decimalsMax)\n }\n}\n\n// NOTE: if we needed for some reason, this could also be defined in\n// a json file that's read in and generates this object. For\n// example, that would allow the addition of currencies without\n// having to \"recompile\" a new version of the app.\nconst currencies = {\n USD: makeCurrency({\n symbol: '$',\n symbolWithCode: '$ USD',\n decimalsMax: 2,\n formatCurrency: amount => '$' + amount\n }),\n EUR: makeCurrency({\n symbol: '\u20AC',\n symbolWithCode: '\u20AC EUR',\n decimalsMax: 2,\n formatCurrency: amount => '\u20AC' + amount\n }),\n BTC: makeCurrency({\n symbol: '\u0243',\n symbolWithCode: '\u0243 BTC',\n decimalsMax: DECIMALS_MAX,\n formatCurrency: amount => amount + '\u0243'\n })\n}\n\nexport default currencies\n", "'use strict'\n\nimport mincomeProportional from './mincome-proportional.js'\nimport minimizeTotalPaymentsCount from './payments-minimizer.js'\nimport { cloneDeep } from '../giLodash.js'\nimport { saferFloat, DECIMALS_MAX } from '../currencies.js'\n\n \n\nconst tinyNum = 1 / Math.pow(10, DECIMALS_MAX)\n\nexport function unadjustedDistribution ({ haveNeeds = [], minimize = true } \n \n ) {\n const distribution = mincomeProportional(haveNeeds)\n return minimize ? minimizeTotalPaymentsCount(distribution) : distribution\n}\n\nexport function adjustedDistribution (\n { distribution, payments, dueOn } \n) {\n distribution = cloneDeep(distribution)\n // ensure the total is set because of how reduceDistribution works\n for (const todo of distribution) {\n todo.total = todo.amount\n }\n distribution = subtractDistributions(distribution, payments)\n // remove any todos for containing miniscule amounts\n // and pledgers who switched sides should have their todos removed\n .filter(todo => todo.amount >= tinyNum)\n for (const todo of distribution) {\n todo.amount = saferFloat(todo.amount)\n todo.total = saferFloat(todo.total)\n todo.partial = todo.total !== todo.amount\n todo.isLate = false\n todo.dueOn = dueOn\n }\n // TODO: add in latePayments to the end of the distribution\n // consider passing in latePayments\n return distribution\n}\n\n// Merges multiple payments between any combinations two of users:\nfunction reduceDistribution (payments ) {\n // Don't modify the payments list/object parameter in-place, as this is not intended:\n payments = cloneDeep(payments)\n for (let i = 0; i < payments.length; i++) {\n const paymentA = payments[i]\n for (let j = i + 1; j < payments.length; j++) {\n const paymentB = payments[j]\n\n // Were paymentA and paymentB between the same two users?\n if ((paymentA.from === paymentB.from && paymentA.to === paymentB.to) ||\n (paymentA.to === paymentB.from && paymentA.from === paymentB.to)) {\n // Add or subtract paymentB's amount to paymentA's amount, depending on the relative\n // direction of the two payments:\n paymentA.amount += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.amount\n paymentA.total += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.total\n // Remove paymentB from payments, and decrement the inner sentinal loop variable:\n payments.splice(j, 1)\n j--\n }\n }\n }\n return payments\n}\n\nfunction addDistributions (paymentsA , paymentsB ) {\n return reduceDistribution([...paymentsA, ...paymentsB])\n}\n\nfunction subtractDistributions (paymentsA , paymentsB ) {\n // Don't modify any payment list/objects parameters in-place, as this is not intended:\n paymentsB = cloneDeep(paymentsB)\n // Reverse the sign of the second operand's amounts so that the final addition is actually subtraction:\n for (const p of paymentsB) {\n p.amount *= -1\n p.total *= -1\n }\n return addDistributions(paymentsA, paymentsB)\n}\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, Errors, L } from '@common/common.js'\nimport votingRules, { ruleType, VOTE_FOR, VOTE_AGAINST, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './shared/voting/rules.js'\nimport proposals, { proposalType, proposalSettingsType, archiveProposal } from './shared/voting/proposals.js'\nimport {\n PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC, STATUS_OPEN, STATUS_CANCELLED, MAX_ARCHIVED_PROPOSALS, PROPOSAL_ARCHIVED,\n INVITE_INITIAL_CREATOR, INVITE_STATUS, PROFILE_STATUS, INVITE_EXPIRES_IN_DAYS\n} from './shared/constants.js'\nimport { paymentStatusType, paymentType, PAYMENT_COMPLETED } from './shared/payments/index.js'\nimport { merge, deepEqualJSONType, omit } from './shared/giLodash.js'\nimport { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, periodStampGivenDate, dateIsWithinPeriod, DAYS_MILLIS } from './shared/time.js'\nimport { unadjustedDistribution, adjustedDistribution } from './shared/distribution/distribution.js'\nimport currencies, { saferFloat } from './shared/currencies.js'\nimport { inviteType, chatRoomAttributesType } from './shared/types.js'\nimport { arrayOf, mapOf, objectOf, objectMaybeOf, optional, string, number, boolean, object, unionOf, tupleOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction vueFetchInitKV (obj , key , initialValue ) {\n let value = obj[key]\n if (!value) {\n Vue.set(obj, key, initialValue)\n value = obj[key]\n }\n return value\n}\n\nfunction initGroupProfile (contractID , joinedDate ) {\n return {\n globalUsername: '', // TODO: this? e.g. groupincome:greg / namecoin:bob / ens:alice\n contractID,\n joinedDate,\n nonMonetaryContributions: [],\n status: PROFILE_STATUS.ACTIVE,\n departedDate: null\n }\n}\n\nfunction initPaymentPeriod ({ getters }) {\n return {\n // this saved so that it can be used when creating a new payment\n initialCurrency: getters.groupMincomeCurrency,\n // TODO: should we also save the first period's currency exchange rate..?\n // all payments during the period use this to set their exchangeRate\n // see notes and code in groupIncomeAdjustedDistribution for details.\n // TODO: for the currency change proposal, have it update the mincomeExchangeRate\n // using .mincomeExchangeRate *= proposal.exchangeRate\n mincomeExchangeRate: 1, // modified by proposals to change mincome currency\n paymentsFrom: {}, // fromUser => toUser => Array\n // snapshot of adjusted distribution after each completed payment\n // yes, it is possible a payment began in one period and completed in another\n // in which case lastAdjustedDistribution for the previous period will be updated\n lastAdjustedDistribution: null,\n // snapshot of haveNeeds. made only when there are no payments\n haveNeedsSnapshot: null\n }\n}\n\n// NOTE: do not call any of these helper functions from within a getter b/c they modify state!\n\nfunction clearOldPayments ({ state, getters }) {\n const sortedPeriodKeys = Object.keys(state.paymentsByPeriod).sort()\n // save two periods worth of payments, max\n while (sortedPeriodKeys.length > 2) {\n const period = sortedPeriodKeys.shift()\n for (const paymentHash of getters.paymentHashesForPeriod(period)) {\n Vue.delete(state.payments, paymentHash)\n // TODO: archive the old payments in a sideEffect, not here\n }\n Vue.delete(state.paymentsByPeriod, period)\n }\n}\n\nfunction initFetchPeriodPayments ({ meta, state, getters }) {\n const period = getters.periodStampGivenDate(meta.createdDate)\n const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ getters }))\n clearOldPayments({ state, getters })\n return periodPayments\n}\n\n// this function is called each time a payment is completed or a user adjusts their income details.\n// TODO: call also when mincome is adjusted\nfunction updateCurrentDistribution ({ meta, state, getters }) {\n const curPeriodPayments = initFetchPeriodPayments({ meta, state, getters })\n const period = getters.periodStampGivenDate(meta.createdDate)\n const noPayments = Object.keys(curPeriodPayments.paymentsFrom).length === 0\n // update distributionDate if we've passed into the next period\n if (comparePeriodStamps(period, getters.groupSettings.distributionDate) > 0) {\n getters.groupSettings.distributionDate = period\n }\n // save haveNeeds if there are no payments or the haveNeeds haven't been saved yet\n if (noPayments || !curPeriodPayments.haveNeedsSnapshot) {\n curPeriodPayments.haveNeedsSnapshot = getters.haveNeedsForThisPeriod(period)\n }\n // if there are payments this period, save the adjusted distribution\n if (!noPayments) {\n updateAdjustedDistribution({ period, getters })\n }\n}\n\nfunction updateAdjustedDistribution ({ period, getters }) {\n const payments = getters.groupPeriodPayments[period]\n if (payments && payments.haveNeedsSnapshot) {\n const minimize = getters.groupSettings.minimizeDistribution\n payments.lastAdjustedDistribution = adjustedDistribution({\n distribution: unadjustedDistribution({ haveNeeds: payments.haveNeedsSnapshot, minimize }),\n payments: getters.paymentsForPeriod(period),\n dueOn: getters.dueDateForPeriod(period)\n }).filter(todo => {\n // only return todos for active members\n return getters.groupProfile(todo.to).status === PROFILE_STATUS.ACTIVE\n })\n }\n}\n\nfunction memberLeaves ({ username, dateLeft }, { meta, state, getters }) {\n state.profiles[username].status = PROFILE_STATUS.REMOVED\n state.profiles[username].departedDate = dateLeft\n // remove any todos for this member from the adjusted distribution\n updateCurrentDistribution({ meta, state, getters })\n}\n\nfunction isActionYoungerThanUser (actionMeta , userProfile ) {\n // A util function that checks if an action (or event) in a group occurred after a particular user joined a group.\n // This is used mostly for checking if a notification should be sent for that user or not.\n // e.g.) user-2 who joined a group later than user-1 (who is the creator of the group) doesn't need to receive\n // 'MEMBER_ADDED' notification for user-1.\n // In some situations, userProfile is undefined, for example, when inviteAccept is called in\n // certain situations. So we need to check for that here.\n return Boolean(userProfile) && compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/group',\n metadata: {\n validate: objectOf({\n createdDate: string,\n username: string,\n identityContractID: string\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n // TODO: We may want to get the time from the server instead of relying on\n // the client in case the client's clock isn't set correctly.\n // the only issue here is that it involves an async function...\n // See: https://github.com/okTurtles/group-income/issues/531\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n // These getters are restricted only to the contract's state.\n // Do not access state outside the contract state inside of them.\n // For example, if the getter you use tries to access `state.loggedIn`,\n // that will break the `latestContractState` function in state.js.\n // It is only safe to access state outside of the contract in a contract action's\n // `sideEffect` function (as long as it doesn't modify contract state)\n getters: {\n // we define `currentGroupState` here so that we can redefine it in state.js\n // so that we can re-use these getter definitions in state.js since they are\n // compatible with Vuex getter definitions.\n // Here `state` refers to the individual group contract's state, the equivalent\n // of `vuexRootState[someGroupContractID]`.\n currentGroupState (state) {\n return state\n },\n groupSettings (state, getters) {\n return getters.currentGroupState.settings || {}\n },\n groupProfile (state, getters) {\n return username => {\n const profiles = getters.currentGroupState.profiles\n return profiles && profiles[username]\n }\n },\n groupProfiles (state, getters) {\n const profiles = {}\n for (const username in (getters.currentGroupState.profiles || {})) {\n const profile = getters.groupProfile(username)\n if (profile.status === PROFILE_STATUS.ACTIVE) {\n profiles[username] = profile\n }\n }\n return profiles\n },\n groupMincomeAmount (state, getters) {\n return getters.groupSettings.mincomeAmount\n },\n groupMincomeCurrency (state, getters) {\n return getters.groupSettings.mincomeCurrency\n },\n periodStampGivenDate (state, getters) {\n return (recentDate ) => {\n if (typeof recentDate !== 'string') {\n recentDate = recentDate.toISOString()\n }\n const { distributionDate, distributionPeriodLength } = getters.groupSettings\n return periodStampGivenDate({\n recentDate,\n periodStart: distributionDate,\n periodLength: distributionPeriodLength\n })\n }\n },\n periodBeforePeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -len))\n }\n },\n periodAfterPeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), len))\n }\n },\n dueDateForPeriod (state, getters) {\n return (periodStamp ) => {\n return dateToPeriodStamp(\n addTimeToDate(\n dateFromPeriodStamp(getters.periodAfterPeriod(periodStamp)),\n -DAYS_MILLIS\n )\n )\n }\n },\n paymentTotalFromUserToUser (state, getters) {\n return (fromUser, toUser, periodStamp) => {\n const payments = getters.currentGroupState.payments\n const periodPayments = getters.groupPeriodPayments\n const { paymentsFrom, mincomeExchangeRate } = periodPayments[periodStamp] || {}\n // NOTE: @babel/plugin-proposal-optional-chaining would come in super-handy\n // here, but I couldn't get it to work with our linter. :(\n // https://github.com/babel/babel-eslint/issues/511\n const total = (((paymentsFrom || {})[fromUser] || {})[toUser] || []).reduce((a, hash) => {\n const payment = payments[hash]\n let { amount, exchangeRate, status } = payment.data\n if (status !== PAYMENT_COMPLETED) {\n return a\n }\n const paymentCreatedPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n // if this payment is from a previous period, then make sure to take into account\n // any proposals that passed in between the payment creation and the payment\n // completion that modified the group currency by multiplying both period's\n // exchange rates\n if (periodStamp !== paymentCreatedPeriodStamp) {\n if (paymentCreatedPeriodStamp !== getters.periodBeforePeriod(periodStamp)) {\n console.warn(`paymentTotalFromUserToUser: super old payment shouldn't exist, ignoring! (curPeriod=${periodStamp})`, JSON.stringify(payment))\n return a\n }\n exchangeRate *= periodPayments[paymentCreatedPeriodStamp].mincomeExchangeRate\n }\n return a + (amount * exchangeRate * mincomeExchangeRate)\n }, 0)\n return saferFloat(total)\n }\n },\n paymentHashesForPeriod (state, getters) {\n return (periodStamp) => {\n const periodPayments = getters.groupPeriodPayments[periodStamp]\n if (periodPayments) {\n let hashes = []\n const { paymentsFrom } = periodPayments\n for (const fromUser in paymentsFrom) {\n for (const toUser in paymentsFrom[fromUser]) {\n hashes = hashes.concat(paymentsFrom[fromUser][toUser])\n }\n }\n return hashes\n }\n }\n },\n groupMembersByUsername (state, getters) {\n return Object.keys(getters.groupProfiles)\n },\n groupMembersCount (state, getters) {\n return getters.groupMembersByUsername.length\n },\n groupMembersPending (state, getters) {\n const invites = getters.currentGroupState.invites\n const pendingMembers = {}\n for (const inviteId in invites) {\n const invite = invites[inviteId]\n if (\n invite.status === INVITE_STATUS.VALID &&\n invite.creator !== INVITE_INITIAL_CREATOR\n ) {\n pendingMembers[invites[inviteId].invitee] = {\n invitedBy: invites[inviteId].creator,\n expires: invite.expires\n }\n }\n }\n return pendingMembers\n },\n groupShouldPropose (state, getters) {\n return getters.groupMembersCount >= 3\n },\n groupProposalSettings (state, getters) {\n return (proposalType = PROPOSAL_GENERIC) => {\n return getters.groupSettings.proposals[proposalType]\n }\n },\n groupCurrency (state, getters) {\n const mincomeCurrency = getters.groupMincomeCurrency\n return mincomeCurrency && currencies[mincomeCurrency]\n },\n groupMincomeFormatted (state, getters) {\n return getters.withGroupCurrency?.(getters.groupMincomeAmount)\n },\n groupMincomeSymbolWithCode (state, getters) {\n return getters.groupCurrency?.symbolWithCode\n },\n groupPeriodPayments (state, getters) {\n // note: a lot of code expects this to return an object, so keep the || {} below\n return getters.currentGroupState.paymentsByPeriod || {}\n },\n withGroupCurrency (state, getters) {\n // TODO: If this group has no defined mincome currency, not even a default one like\n // USD, then calling this function is probably an error which should be reported.\n // Just make sure the UI doesn't break if an exception is thrown, since this is\n // bound to the UI in some location.\n return getters.groupCurrency?.displayWithCurrency\n },\n getChatRooms (state, getters) {\n return getters.currentGroupState.chatRooms\n },\n generalChatRoomId (state, getters) {\n return getters.currentGroupState.generalChatRoomId\n },\n // getter is named haveNeedsForThisPeriod instead of haveNeedsForPeriod because it uses\n // getters.groupProfiles - and that is always based on the most recent values. we still\n // pass in the current period because it's used to set the \"when\" property\n haveNeedsForThisPeriod (state, getters) {\n return (currentPeriod ) => {\n // NOTE: if we ever switch back to the \"real-time\" adjusted distribution algorithm,\n // make sure that this function also handles userExitsGroupEvent\n const groupProfiles = getters.groupProfiles // TODO: these should use the haveNeeds for the specific period's distribution period\n const haveNeeds = []\n for (const username in groupProfiles) {\n const { incomeDetailsType, joinedDate } = groupProfiles[username]\n if (incomeDetailsType) {\n const amount = groupProfiles[username][incomeDetailsType]\n const haveNeed = incomeDetailsType === 'incomeAmount' ? amount - getters.groupMincomeAmount : amount\n // construct 'when' this way in case we ever use a pro-rated algorithm\n let when = dateFromPeriodStamp(currentPeriod).toISOString()\n if (dateIsWithinPeriod({\n date: joinedDate,\n periodStart: currentPeriod,\n periodLength: getters.groupSettings.distributionPeriodLength\n })) {\n when = joinedDate\n }\n haveNeeds.push({ name: username, haveNeed, when })\n }\n }\n return haveNeeds\n }\n },\n paymentsForPeriod (state, getters) {\n return (periodStamp) => {\n const hashes = getters.paymentHashesForPeriod(periodStamp)\n const events = []\n if (hashes && hashes.length > 0) {\n const payments = getters.currentGroupState.payments\n for (const paymentHash of hashes) {\n const payment = payments[paymentHash]\n if (payment.data.status === PAYMENT_COMPLETED) {\n events.push({\n from: payment.meta.username,\n to: payment.data.toUser,\n hash: paymentHash,\n amount: payment.data.amount,\n isLate: !!payment.data.isLate,\n when: payment.data.completedDate\n })\n }\n }\n }\n return events\n }\n }\n // distributionEventsForMonth (state, getters) {\n // return (monthstamp) => {\n // // NOTE: if we ever switch back to the \"real-time\" adjusted distribution\n // // algorithm, make sure that this function also handles userExitsGroupEvent\n // const distributionEvents = getters.haveNeedEventsForMonth(monthstamp)\n // const paymentEvents = getters.paymentEventsForMonth(monthstamp)\n // distributionEvents.splice(distributionEvents.length, 0, paymentEvents)\n // return distributionEvents.sort((a, b) => compareISOTimestamps(a.data.when, b.data.when))\n // }\n // }\n },\n // NOTE: All mutations must be atomic in their edits of the contract state.\n // THEY ARE NOT to farm out any further mutations through the async actions!\n actions: {\n // this is the constructor\n 'gi.contracts/group': {\n validate: objectMaybeOf({\n invites: mapOf(string, inviteType),\n settings: objectMaybeOf({\n // TODO: add 'groupPubkey'\n groupName: string,\n groupPicture: string,\n sharedValues: string,\n mincomeAmount: number,\n mincomeCurrency: string,\n distributionDate: isPeriodStamp,\n distributionPeriodLength: number,\n minimizeDistribution: boolean,\n proposals: objectOf({\n [PROPOSAL_INVITE_MEMBER]: proposalSettingsType,\n [PROPOSAL_REMOVE_MEMBER]: proposalSettingsType,\n [PROPOSAL_GROUP_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_GENERIC]: proposalSettingsType\n })\n })\n }),\n process ({ data, meta }, { state, getters }) {\n // TODO: checkpointing: https://github.com/okTurtles/group-income/issues/354\n const initialState = merge({\n payments: {},\n paymentsByPeriod: {},\n invites: {},\n proposals: {}, // hashes => {} TODO: this, see related TODOs in GroupProposal\n settings: {\n groupCreator: meta.username,\n distributionPeriodLength: 30 * DAYS_MILLIS,\n inviteExpiryOnboarding: INVITE_EXPIRES_IN_DAYS.ON_BOARDING,\n inviteExpiryProposal: INVITE_EXPIRES_IN_DAYS.PROPOSAL\n },\n profiles: {\n [meta.username]: initGroupProfile(meta.identityContractID, meta.createdDate)\n },\n chatRooms: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n initFetchPeriodPayments({ meta, state, getters })\n }\n },\n 'gi.contracts/group/payment': {\n validate: objectMaybeOf({\n // TODO: how to handle donations to okTurtles?\n // TODO: how to handle payments to groups or users outside of this group?\n toUser: string,\n amount: number,\n currencyFromTo: tupleOf(string, string), // must be one of the keys in currencies.js (e.g. USD, EUR, etc.) TODO: handle old clients not having one of these keys, see OP_PROTOCOL_UPGRADE https://github.com/okTurtles/group-income/issues/603\n // multiply 'amount' by 'exchangeRate', which must always be\n // based on the initialCurrency of the period in which this payment was created.\n // it is then further multiplied by the period's 'mincomeExchangeRate', which\n // is modified if any proposals pass to change the mincomeCurrency\n exchangeRate: number,\n txid: string,\n status: paymentStatusType,\n paymentType: paymentType,\n details: optional(object),\n memo: optional(string)\n }),\n process ({ data, meta, hash }, { state, getters }) {\n if (data.status === PAYMENT_COMPLETED) {\n console.error(`payment: payment ${hash} cannot have status = 'completed'!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('payments cannot be instantly completed!')\n }\n Vue.set(state.payments, hash, {\n data: {\n ...data,\n groupMincome: getters.groupMincomeAmount\n },\n meta,\n history: [[meta.createdDate, hash]]\n })\n const { paymentsFrom } = initFetchPeriodPayments({ meta, state, getters })\n const fromUser = vueFetchInitKV(paymentsFrom, meta.username, {})\n const toUser = vueFetchInitKV(fromUser, data.toUser, [])\n toUser.push(hash)\n // TODO: handle completed payments here too! (for manual payment support)\n }\n },\n 'gi.contracts/group/paymentUpdate': {\n validate: objectMaybeOf({\n paymentHash: string,\n updatedProperties: objectMaybeOf({\n status: paymentStatusType,\n details: object,\n memo: string\n })\n }),\n process ({ data, meta, hash }, { state, getters }) {\n // TODO: we don't want to keep a history of all payments in memory all the time\n // https://github.com/okTurtles/group-income/issues/426\n const payment = state.payments[data.paymentHash]\n // TODO: move these types of validation errors into the validate function so\n // that they can be done before sending as well as upon receiving\n if (!payment) {\n console.error(`paymentUpdate: no payment ${data.paymentHash}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate without existing payment')\n }\n // if the payment is being modified by someone other than the person who sent or received it, throw an exception\n if (meta.username !== payment.meta.username && meta.username !== payment.data.toUser) {\n console.error(`paymentUpdate: bad username ${meta.username} != ${payment.meta.username} != ${payment.data.username}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate from bad user!')\n }\n payment.history.push([meta.createdDate, hash])\n merge(payment.data, data.updatedProperties)\n // we update \"this period\"'s snapshot 'lastAdjustedDistribution' on each completed payment\n if (data.updatedProperties.status === PAYMENT_COMPLETED) {\n payment.data.completedDate = meta.createdDate\n // update the current distribution unless this update is for a payment from the previous period\n const updatePeriodStamp = getters.periodStampGivenDate(meta.createdDate)\n const paymentPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n if (comparePeriodStamps(updatePeriodStamp, paymentPeriodStamp) > 0) {\n updateAdjustedDistribution({ period: paymentPeriodStamp, getters })\n } else {\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n }\n },\n 'gi.contracts/group/proposal': {\n validate: (data, { state, meta }) => {\n objectOf({\n proposalType: proposalType,\n proposalData: object, // data for Vue widgets\n votingRule: ruleType,\n expires_date_ms: number // calculate by grabbing proposal expiry from group properties and add to `meta.createdDate`\n })(data)\n\n const dataToCompare = omit(data.proposalData, ['reason'])\n\n // Validate this isn't a duplicate proposal\n for (const hash in state.proposals) {\n const prop = state.proposals[hash]\n if (prop.status !== STATUS_OPEN || prop.data.proposalType !== data.proposalType) {\n continue\n }\n\n if (deepEqualJSONType(omit(prop.data.proposalData, ['reason']), dataToCompare)) {\n throw new TypeError(L('There is an identical open proposal.'))\n }\n\n // TODO - verify if type of proposal already exists (SETTING_CHANGE).\n }\n },\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.proposals, hash, {\n data,\n meta,\n votes: { [meta.username]: VOTE_FOR },\n status: STATUS_OPEN,\n payload: null // set later by group/proposalVote\n })\n // TODO: save all proposals disk so that we only keep open proposals in memory\n // TODO: create a global timer to auto-pass/archive expired votes\n // make sure to set that proposal's status as STATUS_EXPIRED if it's expired\n },\n sideEffect ({ contractID, meta, data }, { getters }) {\n const { loggedIn } = sbp('state/vuex/state')\n const typeToSubTypeMap = {\n [PROPOSAL_INVITE_MEMBER]: 'ADD_MEMBER',\n [PROPOSAL_REMOVE_MEMBER]: 'REMOVE_MEMBER',\n [PROPOSAL_GROUP_SETTING_CHANGE]: 'CHANGE_MINCOME',\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: 'CHANGE_VOTING_RULE',\n [PROPOSAL_GENERIC]: 'GENERIC'\n }\n\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'NEW_PROPOSAL', {\n groupID: contractID,\n creator: meta.username,\n subtype: typeToSubTypeMap[data.proposalType]\n })\n }\n }\n },\n 'gi.contracts/group/proposalVote': {\n validate: objectOf({\n proposalHash: string,\n vote: string,\n passPayload: optional(unionOf(object, string)) // TODO: this, somehow we need to send an OP_KEY_ADD GIMessage to add a generated once-only writeonly message public key to the contract, and (encrypted) include the corresponding invite link, also, we need all clients to verify that this message/operation was valid to prevent a hacked client from adding arbitrary OP_KEY_ADD messages, and automatically ban anyone generating such messages\n }),\n process (message, { state }) {\n const { data, hash, meta } = message\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalVote: no proposal for ${data.proposalHash}!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n }\n Vue.set(proposal.votes, meta.username, data.vote)\n // TODO: handle vote pass/fail\n // check if proposal is expired, if so, ignore (but log vote)\n if (new Date(meta.createdDate).getTime() > proposal.data.expires_date_ms) {\n console.warn('proposalVote: vote on expired proposal!', { proposal, data, meta })\n // TODO: display warning or something\n return\n }\n // see if this is a deciding vote\n const result = votingRules[proposal.data.votingRule](state, proposal.data.proposalType, proposal.votes)\n if (result === VOTE_FOR || result === VOTE_AGAINST) {\n // handles proposal pass or fail, will update proposal.status accordingly\n proposals[proposal.data.proposalType][result](state, message)\n Vue.set(proposal, 'dateClosed', meta.createdDate)\n }\n },\n sideEffect ({ contractID, data, meta }, { state, getters }) {\n const proposal = state.proposals[data.proposalHash]\n const { loggedIn } = sbp('state/vuex/state')\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (proposal?.dateClosed &&\n isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'PROPOSAL_CLOSED', {\n groupID: contractID,\n creator: meta.username,\n proposalStatus: proposal.status\n })\n }\n }\n },\n 'gi.contracts/group/proposalCancel': {\n validate: objectOf({\n proposalHash: string\n }),\n process ({ data, meta, contractID }, { state }) {\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalCancel: no proposal for ${data.proposalHash}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n } else if (proposal.meta.username !== meta.username) {\n console.error(`proposalCancel: proposal ${data.proposalHash} belongs to ${proposal.meta.username} not ${meta.username}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalWithdraw for wrong user!')\n }\n Vue.set(proposal, 'status', STATUS_CANCELLED)\n archiveProposal({ state, proposalHash: data.proposalHash, proposal, contractID })\n }\n },\n 'gi.contracts/group/removeMember': {\n validate: (data, { state, getters, meta }) => {\n objectOf({\n member: string, // username to remove\n reason: optional(string),\n automated: optional(boolean),\n // In case it happens in a big group (by proposal)\n // we need to validate the associated proposal.\n proposalHash: optional(string),\n proposalPayload: optional(objectOf({\n secret: string // NOTE: simulate the OP_KEY_* stuff for now\n }))\n })(data)\n\n const memberToRemove = data.member\n const membersCount = getters.groupMembersCount\n\n if (!state.profiles[memberToRemove]) {\n throw new TypeError(L('Not part of the group.'))\n }\n if (membersCount === 1 || memberToRemove === meta.username) {\n throw new TypeError(L('Cannot remove yourself.'))\n }\n\n if (membersCount < 3) {\n // In a small group only the creator can remove someone\n // TODO: check whether meta.username has required admin permissions\n if (meta.username !== state.settings.groupCreator) {\n throw new TypeError(L('Only the group creator can remove members.'))\n }\n } else {\n // In a big group a removal can only happen through a proposal\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // TODO this\n throw new TypeError(L('Admin credentials needed and not implemented yet.'))\n }\n\n if (!proposal.payload || proposal.payload.secret !== data.proposalPayload.secret) {\n throw new TypeError(L('Invalid associated proposal.'))\n }\n }\n },\n process ({ data, meta }, { state, getters }) {\n memberLeaves(\n { username: data.member, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n },\n sideEffect ({ data, meta, contractID }, { state, getters }) {\n const rootState = sbp('state/vuex/state')\n const contracts = rootState.contracts || {}\n const { username } = rootState.loggedIn\n\n if (data.member === username) {\n // If this member is re-joining the group, ignore the rest\n // so the member doesn't remove themself again.\n if (sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n return\n }\n\n const groupIdToSwitch = Object.keys(contracts)\n .find(cID => contracts[cID].type === 'gi.contracts/group' &&\n cID !== contractID && rootState[cID].settings) || null\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {})\n sbp('state/vuex/commit', 'setCurrentGroupId', groupIdToSwitch)\n // we can't await on this in here, because it will cause a deadlock, since Chelonia processes\n // this sideEffect on the eventqueue for this contractID, and /remove uses that same eventqueue\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown by /remove ${contractID}:`, e)\n })\n // this looks crazy, but doing this was necessary to fix a race condition in the\n // group-member-removal Cypress tests where due to the ordering of asynchronous events\n // we were getting the same latestHash upon re-logging in for test \"user2 rejoins groupA\".\n // We add it to the same queue as '/remove' above gets run on so that it is run after\n // contractID is removed. See also comments in 'gi.actions/identity/login'.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/saveOurLoginState'])\n .then(function () {\n const router = sbp('controller/router')\n const switchFrom = router.currentRoute.path\n const switchTo = groupIdToSwitch ? '/dashboard' : '/'\n if (switchFrom !== '/join' && switchFrom !== switchTo) {\n router.push({ path: switchTo }).catch(console.warn)\n }\n }).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown during queueEvent to ${contractID} by saveOurLoginState:`, e)\n })\n // TODO - #828 remove other group members contracts if applicable\n } else {\n const myProfile = getters.groupProfile(username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n const memberRemovedThemselves = data.member === meta.username\n\n sbp('gi.notifications/emit', // emit a notification for a member removal.\n memberRemovedThemselves ? 'MEMBER_LEFT' : 'MEMBER_REMOVED',\n {\n groupID: contractID,\n username: memberRemovedThemselves ? meta.username : data.member\n })\n }\n // TODO - #828 remove the member contract if applicable.\n // problem is, if they're in another group we're also a part of, or if we\n // have a DM with them, we don't want to do this. may need to use manual reference counting\n // sbp('chelonia/contract/release', getters.groupProfile(data.member).contractID)\n }\n // TODO - #850 verify open proposals and see if they need some re-adjustment.\n }\n },\n 'gi.contracts/group/removeOurselves': {\n validate: objectMaybeOf({\n reason: string\n }),\n process ({ data, meta, contractID }, { state, getters }) {\n memberLeaves(\n { username: meta.username, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', {\n meta,\n data: { member: meta.username, reason: data.reason || '' },\n contractID\n }]\n )\n }\n },\n 'gi.contracts/group/invite': {\n validate: inviteType,\n process ({ data, meta }, { state }) {\n Vue.set(state.invites, data.inviteSecret, data)\n }\n },\n 'gi.contracts/group/inviteAccept': {\n validate: objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n }),\n process ({ data, meta }, { state }) {\n console.debug('inviteAccept:', data, state.invites)\n const invite = state.invites[data.inviteSecret]\n if (invite.status !== INVITE_STATUS.VALID) {\n console.error(`inviteAccept: invite for ${meta.username} is: ${invite.status}`)\n return\n }\n Vue.set(invite.responses, meta.username, true)\n if (Object.keys(invite.responses).length === invite.quantity) {\n invite.status = INVITE_STATUS.USED\n }\n // TODO: ensure `meta.username` is unique for the lifetime of the username\n // since we are making it possible for the same username to leave and\n // rejoin the group. All of their past posts will be re-associated with\n // them upon re-joining.\n Vue.set(state.profiles, meta.username, initGroupProfile(meta.identityContractID, meta.createdDate))\n // If we're triggered by handleEvent in state.js (and not latestContractState)\n // then the asynchronous sideEffect function will get called next\n // and we will subscribe to this new user's identity contract\n },\n // !! IMPORANT!!\n // Actions here MUST NOT modify contract state!\n // They MUST NOT call 'commit'!\n // They should only coordinate the actions of outside contracts.\n // Otherwise `latestContractState` and `handleEvent` will not produce same state!\n async sideEffect ({ meta, contractID }, { state }) {\n const { loggedIn } = sbp('state/vuex/state')\n const { profiles = {} } = state\n\n // TODO: per #257 this will ,have to be encompassed in a recoverable transaction\n // however per #610 that might be handled in handleEvent (?), or per #356 might not be needed\n if (meta.username === loggedIn.username) {\n // we're the person who just accepted the group invite\n // so subscribe to founder's IdentityContract & everyone else's\n for (const name in profiles) {\n if (name !== loggedIn.username) {\n await sbp('chelonia/contract/sync', profiles[name].contractID)\n }\n }\n } else {\n const myProfile = profiles[loggedIn.username]\n // we're an existing member of the group getting notified that a\n // new member has joined, so subscribe to their identity contract\n await sbp('chelonia/contract/sync', meta.identityContractID)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'MEMBER_ADDED', { // emit a notification for a member addition.\n groupID: contractID,\n username: meta.username\n })\n }\n }\n }\n },\n 'gi.contracts/group/inviteRevoke': {\n validate: (data, { state, meta }) => {\n objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n })(data)\n\n if (!state.invites[data.inviteSecret]) {\n throw new TypeError(L('The link does not exist.'))\n }\n },\n process ({ data, meta }, { state }) {\n const invite = state.invites[data.inviteSecret]\n Vue.set(invite, 'status', INVITE_STATUS.REVOKED)\n }\n },\n 'gi.contracts/group/updateSettings': {\n // OPTIMIZE: Make this custom validation function\n // reusable accross other future validators\n validate: objectMaybeOf({\n groupName: x => typeof x === 'string',\n groupPicture: x => typeof x === 'string',\n sharedValues: x => typeof x === 'string',\n mincomeAmount: x => typeof x === 'number' && x > 0,\n mincomeCurrency: x => typeof x === 'string'\n }),\n process ({ meta, data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/group/groupProfileUpdate': {\n validate: objectMaybeOf({\n incomeDetailsType: x => ['incomeAmount', 'pledgeAmount'].includes(x),\n incomeAmount: x => typeof x === 'number' && x >= 0,\n pledgeAmount: x => typeof x === 'number' && x >= 0,\n nonMonetaryAdd: string,\n nonMonetaryEdit: objectOf({\n replace: string,\n with: string\n }),\n nonMonetaryRemove: string,\n paymentMethods: arrayOf(\n objectOf({\n name: string,\n value: string\n })\n )\n }),\n process ({ data, meta }, { state, getters }) {\n const groupProfile = state.profiles[meta.username]\n const nonMonetary = groupProfile.nonMonetaryContributions\n for (const key in data) {\n const value = data[key]\n switch (key) {\n case 'nonMonetaryAdd':\n nonMonetary.push(value)\n break\n case 'nonMonetaryRemove':\n nonMonetary.splice(nonMonetary.indexOf(value), 1)\n break\n case 'nonMonetaryEdit':\n nonMonetary.splice(nonMonetary.indexOf(value.replace), 1, value.with)\n break\n default:\n Vue.set(groupProfile, key, value)\n }\n }\n if (data.incomeDetailsType) {\n // someone updated their income details, create a snapshot of the haveNeeds\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n },\n 'gi.contracts/group/updateAllVotingRules': {\n validate: objectMaybeOf({\n ruleName: x => [RULE_PERCENTAGE, RULE_DISAGREEMENT].includes(x),\n ruleThreshold: number,\n expires_ms: number\n }),\n process ({ data, meta }, { state }) {\n // Update all types of proposal settings for simplicity\n if (data.ruleName && data.ruleThreshold) {\n for (const proposalSettings in state.settings.proposals) {\n Vue.set(state.settings.proposals[proposalSettings], 'rule', data.ruleName)\n Vue.set(state.settings.proposals[proposalSettings].ruleSettings[data.ruleName], 'threshold', data.ruleThreshold)\n }\n }\n\n // TODO later - support update expires_ms\n // if (data.ruleName && data.expires_ms) {\n // for (const proposalSetting in state.settings.proposals) {\n // Vue.set(state.settings.proposals[proposalSetting].ruleSettings[data.ruleName], 'expires_ms', data.expires_ms)\n // }\n // }\n }\n },\n 'gi.contracts/group/addChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n attributes: chatRoomAttributesType\n }),\n process ({ data, meta }, { state }) {\n const { name, type, privacyLevel } = data.attributes\n Vue.set(state.chatRooms, data.chatRoomID, {\n creator: meta.username,\n name,\n type,\n privacyLevel,\n deletedDate: null,\n users: []\n })\n if (!state.generalChatRoomId) {\n Vue.set(state, 'generalChatRoomId', data.chatRoomID)\n }\n }\n },\n 'gi.contracts/group/deleteChatRoom': {\n validate: (data, { getters, meta }) => {\n objectOf({ chatRoomID: string })(data)\n\n if (getters.getChatRooms[data.chatRoomID].creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state }) {\n Vue.delete(state.chatRooms, data.chatRoomID)\n }\n },\n 'gi.contracts/group/leaveChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n member: string,\n leavingGroup: boolean // if kicker is exists, it means group leaving\n }),\n process ({ data, meta }, { state }) {\n Vue.set(state.chatRooms[data.chatRoomID], 'users',\n state.chatRooms[data.chatRoomID].users.filter(u => u !== data.member))\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (meta.username === rootState.loggedIn.username && !sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n const sendingData = data.leavingGroup\n ? { member: data.member }\n : { member: data.member, username: meta.username }\n await sbp('gi.actions/chatroom/leave', { contractID: data.chatRoomID, data: sendingData })\n }\n }\n },\n 'gi.contracts/group/joinChatRoom': {\n validate: objectMaybeOf({\n username: string,\n chatRoomID: string\n }),\n process ({ data, meta }, { state }) {\n const username = data.username || meta.username\n state.chatRooms[data.chatRoomID].users.push(username)\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n const username = data.username || meta.username\n if (username === rootState.loggedIn.username) {\n if (!sbp('okTurtles.data/get', 'JOINING_GROUP') || sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n // while users are joining chatroom, they don't need to leave chatrooms\n // this is similar to setting 'JOINING_GROUP' before joining group\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', data.chatRoomID)\n await sbp('chelonia/contract/sync', data.chatRoomID)\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', undefined)\n sbp('okTurtles.data/set', 'READY_TO_JOIN_CHATROOM', false)\n }\n }\n }\n },\n 'gi.contracts/group/renameChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n name: string\n }),\n process ({ data, meta }, { state, getters }) {\n Vue.set(state.chatRooms, data.chatRoomID, {\n ...getters.getChatRooms[data.chatRoomID],\n name: data.name\n })\n }\n },\n ...((process.env.NODE_ENV === 'development' || process.env.CI) && {\n 'gi.contracts/group/forceDistributionDate': {\n validate: optional,\n process ({ meta }, { state, getters }) {\n getters.groupSettings.distributionDate = dateToPeriodStamp(meta.createdDate)\n }\n },\n 'gi.contracts/group/malformedMutation': {\n validate: objectOf({ errorType: string, sideEffect: optional(boolean) }),\n process ({ data }) {\n const ErrorType = Errors[data.errorType]\n if (data.sideEffect) return\n if (ErrorType) {\n throw new ErrorType('malformedMutation!')\n } else {\n throw new Error(`unknown error type: ${data.errorType}`)\n }\n },\n sideEffect (message, { state }) {\n if (!message.data.sideEffect) return\n sbp('gi.contracts/group/malformedMutation/process', {\n ...message,\n data: omit(message.data, ['sideEffect'])\n }, state)\n }\n }\n })\n // TODO: remove group profile when leave group is implemented\n },\n // methods are SBP selectors that are version-tracked for each contract.\n // in other words, you can use them to define SBP selectors that will\n // contain functions that you can modify across different contract versions,\n // and when the contract calls them, it will use that specific version of the\n // method.\n //\n // They are useful when used in conjunction with pushSideEffect from process\n // functions.\n //\n // IMPORTANT: they MUST begin with the name of the contract.\n methods: {\n 'gi.contracts/group/archiveProposal': async function (contractID, proposalHash, proposal) {\n const { username } = sbp('state/vuex/state').loggedIn\n const key = `proposals/${username}/${contractID}`\n const proposals = await sbp('gi.db/archive/load', key) || []\n // newest at the front of the array, oldest at the back\n proposals.unshift([proposalHash, proposal])\n while (proposals.length > MAX_ARCHIVED_PROPOSALS) {\n proposals.pop()\n }\n await sbp('gi.db/archive/save', key, proposals)\n sbp('okTurtles.events/emit', PROPOSAL_ARCHIVED, [proposalHash, proposal])\n }\n }\n})\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/identity.js b/test/contracts/identity.js index f2213bfc0b..406d8042aa 100644 --- a/test/contracts/identity.js +++ b/test/contracts/identity.js @@ -345,14 +345,14 @@ var objectOf = (typeObj, _scope = "Object") => { } const undefAttr = typeAttrs.find((property) => { const propertyTypeFn = typeObj[property]; - return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); }); if (undefAttr) { throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); } const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { const typeFn = typeObj[key]; - if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { return Object.assign(acc, {}); } else { return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); @@ -361,7 +361,10 @@ var objectOf = (typeObj, _scope = "Object") => { return typeAttrs.reduce(reducer, {}); } object2.type = () => { - const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); return `{| ${props.join(",\n ")} |}`; diff --git a/test/contracts/identity.js.map b/test/contracts/identity.js.map index 55d720bdeb..f0dec6c3c3 100644 --- a/test/contracts/identity.js.map +++ b/test/contracts/identity.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/validators.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/identity.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "// Matches strings of plain ASCII letters and digits, hyphens and underscores.\nexport const allowedUsernameCharacters = (value ) => /^[\\w-]*$/.test(value)\n\n// Matches non-empty strings of plain ASCII letters and digits.\nexport const alphanumeric = (value ) => /^[A-Za-z\\d]+$/.test(value)\n\nexport const noConsecutiveHyphensOrUnderscores = (value ) => !value.includes('--') && !value.includes('__')\n\nexport const noLeadingOrTrailingHyphen = (value ) => !value.startsWith('-') && !value.endsWith('-')\nexport const noLeadingOrTrailingUnderscore = (value ) => !value.startsWith('_') && !value.endsWith('_')\n\nexport const decimals = (digits ) => (value ) => Number.isInteger(value * Math.pow(10, digits))\n\nexport const noUppercase = (value ) => value.toLowerCase() === value\n\nexport const noWhitespace = (value ) => /^\\S+$/.test(value)\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\n\nimport { Vue, L } from '@common/common.js'\nimport { merge } from './shared/giLodash.js'\nimport { objectOf, objectMaybeOf, arrayOf, string, object } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n allowedUsernameCharacters,\n noConsecutiveHyphensOrUnderscores,\n noLeadingOrTrailingHyphen,\n noLeadingOrTrailingUnderscore,\n noUppercase\n} from './shared/validators.js'\n\nimport { IDENTITY_USERNAME_MAX_CHARS } from './shared/constants.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/identity',\n getters: {\n currentIdentityState (state) {\n return state\n },\n loginState (state, getters) {\n return getters.currentIdentityState.loginState\n }\n },\n actions: {\n 'gi.contracts/identity': {\n validate: (data, { state, meta }) => {\n objectMaybeOf({\n attributes: objectMaybeOf({\n username: string,\n email: string,\n picture: string\n })\n })(data)\n const { username } = data.attributes\n if (username.length > IDENTITY_USERNAME_MAX_CHARS) {\n throw new TypeError(`A username cannot exceed ${IDENTITY_USERNAME_MAX_CHARS} characters.`)\n }\n if (!allowedUsernameCharacters(username)) {\n throw new TypeError('A username cannot contain disallowed characters.')\n }\n if (!noConsecutiveHyphensOrUnderscores(username)) {\n throw new TypeError('A username cannot contain two consecutive hyphens or underscores.')\n }\n if (!noLeadingOrTrailingHyphen(username)) {\n throw new TypeError('A username cannot start or end with a hyphen.')\n }\n if (!noLeadingOrTrailingUnderscore(username)) {\n throw new TypeError('A username cannot start or end with an underscore.')\n }\n if (!noUppercase(username)) {\n throw new TypeError('A username cannot contain uppercase letters.')\n }\n },\n process ({ data }, { state }) {\n const initialState = merge({\n settings: {},\n attributes: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/identity/setAttributes': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.attributes, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/deleteAttributes': {\n validate: arrayOf(string),\n process ({ data }, { state }) {\n for (const attribute of data) {\n Vue.delete(state.attributes, attribute)\n }\n }\n },\n 'gi.contracts/identity/updateSettings': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/setLoginState': {\n validate: objectOf({\n groupIds: arrayOf(string)\n }),\n process ({ data }, { state }) {\n Vue.set(state, 'loginState', data)\n },\n sideEffect ({ contractID }) {\n // it only makes sense to call updateLoginStateUponLogin for ourselves\n if (contractID === sbp('state/vuex/getters').ourIdentityContractId) {\n // makes sure that updateLoginStateUponLogin gets run after the entire identity\n // state has been synced, this way we don't end up joining groups we've left, etc.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/updateLoginStateUponLogin'])\n .catch((e) => {\n sbp('gi.notifications/emit', 'ERROR', {\n message: L(\"Failed to join groups we're part of on another device. Not catastrophic, but could lead to problems. {errName}: '{errMsg}'\", {\n errName: e.name,\n errMsg: e.message || '?'\n })\n })\n })\n }\n }\n }\n }\n})\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "// Matches strings of plain ASCII letters and digits, hyphens and underscores.\nexport const allowedUsernameCharacters = (value ) => /^[\\w-]*$/.test(value)\n\n// Matches non-empty strings of plain ASCII letters and digits.\nexport const alphanumeric = (value ) => /^[A-Za-z\\d]+$/.test(value)\n\nexport const noConsecutiveHyphensOrUnderscores = (value ) => !value.includes('--') && !value.includes('__')\n\nexport const noLeadingOrTrailingHyphen = (value ) => !value.startsWith('-') && !value.endsWith('-')\nexport const noLeadingOrTrailingUnderscore = (value ) => !value.startsWith('_') && !value.endsWith('_')\n\nexport const decimals = (digits ) => (value ) => Number.isInteger(value * Math.pow(10, digits))\n\nexport const noUppercase = (value ) => value.toLowerCase() === value\n\nexport const noWhitespace = (value ) => /^\\S+$/.test(value)\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\n\nimport { Vue, L } from '@common/common.js'\nimport { merge } from './shared/giLodash.js'\nimport { objectOf, objectMaybeOf, arrayOf, string, object } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n allowedUsernameCharacters,\n noConsecutiveHyphensOrUnderscores,\n noLeadingOrTrailingHyphen,\n noLeadingOrTrailingUnderscore,\n noUppercase\n} from './shared/validators.js'\n\nimport { IDENTITY_USERNAME_MAX_CHARS } from './shared/constants.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/identity',\n getters: {\n currentIdentityState (state) {\n return state\n },\n loginState (state, getters) {\n return getters.currentIdentityState.loginState\n }\n },\n actions: {\n 'gi.contracts/identity': {\n validate: (data, { state, meta }) => {\n objectMaybeOf({\n attributes: objectMaybeOf({\n username: string,\n email: string,\n picture: string\n })\n })(data)\n const { username } = data.attributes\n if (username.length > IDENTITY_USERNAME_MAX_CHARS) {\n throw new TypeError(`A username cannot exceed ${IDENTITY_USERNAME_MAX_CHARS} characters.`)\n }\n if (!allowedUsernameCharacters(username)) {\n throw new TypeError('A username cannot contain disallowed characters.')\n }\n if (!noConsecutiveHyphensOrUnderscores(username)) {\n throw new TypeError('A username cannot contain two consecutive hyphens or underscores.')\n }\n if (!noLeadingOrTrailingHyphen(username)) {\n throw new TypeError('A username cannot start or end with a hyphen.')\n }\n if (!noLeadingOrTrailingUnderscore(username)) {\n throw new TypeError('A username cannot start or end with an underscore.')\n }\n if (!noUppercase(username)) {\n throw new TypeError('A username cannot contain uppercase letters.')\n }\n },\n process ({ data }, { state }) {\n const initialState = merge({\n settings: {},\n attributes: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/identity/setAttributes': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.attributes, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/deleteAttributes': {\n validate: arrayOf(string),\n process ({ data }, { state }) {\n for (const attribute of data) {\n Vue.delete(state.attributes, attribute)\n }\n }\n },\n 'gi.contracts/identity/updateSettings': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/setLoginState': {\n validate: objectOf({\n groupIds: arrayOf(string)\n }),\n process ({ data }, { state }) {\n Vue.set(state, 'loginState', data)\n },\n sideEffect ({ contractID }) {\n // it only makes sense to call updateLoginStateUponLogin for ourselves\n if (contractID === sbp('state/vuex/getters').ourIdentityContractId) {\n // makes sure that updateLoginStateUponLogin gets run after the entire identity\n // state has been synced, this way we don't end up joining groups we've left, etc.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/updateLoginStateUponLogin'])\n .catch((e) => {\n sbp('gi.notifications/emit', 'ERROR', {\n message: L(\"Failed to join groups we're part of on another device. Not catastrophic, but could lead to problems. {errName}: '{errMsg}'\", {\n errName: e.name,\n errMsg: e.message || '?'\n })\n })\n })\n }\n }\n }\n }\n})\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/mailbox.js b/test/contracts/mailbox.js index 6c0b51bdf7..f37c3df80a 100644 --- a/test/contracts/mailbox.js +++ b/test/contracts/mailbox.js @@ -359,14 +359,14 @@ var objectOf = (typeObj, _scope = "Object") => { } const undefAttr = typeAttrs.find((property) => { const propertyTypeFn = typeObj[property]; - return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); }); if (undefAttr) { throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); } const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { const typeFn = typeObj[key]; - if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { return Object.assign(acc, {}); } else { return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); @@ -375,7 +375,10 @@ var objectOf = (typeObj, _scope = "Object") => { return typeAttrs.reduce(reducer, {}); } object2.type = () => { - const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); return `{| ${props.join(",\n ")} |}`; diff --git a/test/contracts/mailbox.js.map b/test/contracts/mailbox.js.map index 0b239b5707..25423b0e63 100644 --- a/test/contracts/mailbox.js.map +++ b/test/contracts/mailbox.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/mailbox.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { mailType } from './shared/types.js'\nimport { objectOf, string, object, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/mailbox',\n metadata: {\n // TODO: why is this missing the from username..?\n validate: objectOf({\n createdDate: string\n }),\n create () {\n return {\n createdDate: new Date().toISOString()\n }\n }\n },\n actions: {\n 'gi.contracts/mailbox': {\n validate: object, // TODO: define this\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state, key, data[key])\n }\n Vue.set(state, 'messages', [])\n }\n },\n 'gi.contracts/mailbox/postMessage': {\n validate: objectOf({\n messageType: mailType,\n from: string,\n subject: optional(string),\n message: optional(string),\n headers: optional(object)\n }),\n process (message, { state }) {\n state.messages.push(message)\n }\n },\n 'gi.contracts/mailbox/authorizeSender': {\n validate: objectOf({\n sender: string\n }),\n process ({ data }, { state }) {\n // TODO: replace this via OP_KEY_*?\n throw new Error('unimplemented!')\n }\n }\n }\n})\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { mailType } from './shared/types.js'\nimport { objectOf, string, object, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/mailbox',\n metadata: {\n // TODO: why is this missing the from username..?\n validate: objectOf({\n createdDate: string\n }),\n create () {\n return {\n createdDate: new Date().toISOString()\n }\n }\n },\n actions: {\n 'gi.contracts/mailbox': {\n validate: object, // TODO: define this\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state, key, data[key])\n }\n Vue.set(state, 'messages', [])\n }\n },\n 'gi.contracts/mailbox/postMessage': {\n validate: objectOf({\n messageType: mailType,\n from: string,\n subject: optional(string),\n message: optional(string),\n headers: optional(object)\n }),\n process (message, { state }) {\n state.messages.push(message)\n }\n },\n 'gi.contracts/mailbox/authorizeSender': {\n validate: objectOf({\n sender: string\n }),\n process ({ data }, { state }) {\n // TODO: replace this via OP_KEY_*?\n throw new Error('unimplemented!')\n }\n }\n }\n})\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/shared/payments/index.js.map b/test/contracts/shared/payments/index.js.map index 114ff938c7..527eb79258 100644 --- a/test/contracts/shared/payments/index.js.map +++ b/test/contracts/shared/payments/index.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../../../frontend/model/contracts/misc/flowTyper.js", "../../../../frontend/model/contracts/shared/payments/index.js"], - "sourcesContent": ["// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n"], - "mappings": "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", + "sourcesContent": ["// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n"], + "mappings": ";;;AAmDO,IAAM,cAAc,OAAO,SAAS;AACpC,IAAM,UAAU,OAAK,MAAM;AAE3B,IAAM,UAAU,OAAK,OAAO,MAAM;AAClC,IAAM,YAAY,OAAK,OAAO,MAAM;AAIpC,IAAM,aAAa,OAAK,OAAO,MAAM;AAcrC,IAAM,UAAU,CAAC,QAAQ,aAAa;AAC3C,MAAI,WAAW,OAAO,IAAI;AAAG,WAAO,OAAO,KAAK,QAAQ;AACxD,SAAO,OAAO,QAAQ;AACxB;AAGO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YACE,SACA,cACA,WACA,OACA,WAAmB,IACnB,YAAqB,IACrB;AACA,UAAM,aAAa,WACjB,YAAY,0BAA0B,YAAY;AACpD,UAAM,UAAU;AAChB,SAAK,eAAe;AACpB,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,YAAY,aAAa;AAC9B,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,UAAU,GAAG;AAAA,EAAe,KAAK,aAAa;AACnD,SAAK,OAAO,KAAK,YAAY;AAC7B,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,kBAAkB;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,gBAAyB;AACvB,UAAM,YAAY,KAAK,MAAM,MAAM,gCAAgC,KAAK,CAAC;AACzE,WAAO,UAAU,KAAK,cAAY,SAAS,QAAQ,qBAAqB,MAAM,EAAE,KAAK;AAAA,EACvF;AAAA,EAEA,eAAwB;AACtB,WAAO;AAAA,eACI,KAAK;AAAA,eACL,KAAK;AAAA,eACL,KAAK,aAAa,QAAQ,OAAO,EAAE;AAAA,eACnC,KAAK;AAAA,eACL,KAAK;AAAA;AAAA,EAElB;AACF;AAKA,IAAM,iBAAoB,CACxB,QACA,OACA,OACA,SACA,cACA,cACuB;AACvB,SAAO,IAAI,mBACT,SACA,gBAAgB,QAAQ,MAAM,GAC9B,aAAa,OAAO,OACpB,KAAK,UAAU,KAAK,GACpB,OAAO,MACP,KACF;AACF;AAgBO,IAAM,YACM,CAAC,cAAmC;AACnD,mBAAkB,OAAO,SAAS,IAAI;AACpC,QAAI,QAAQ,KAAK,KAAM,UAAU;AAAY,aAAO;AACpD,UAAM,eAAe,SAAS,OAAO,MAAM;AAAA,EAC7C;AACA,UAAQ,OAAO,MAAM;AACnB,QAAI,UAAU,SAAS;AAAG,aAAO,GAAG,YAAY,SAAS;AAAA;AACpD,aAAO,IAAI;AAAA,EAClB;AACA,SAAO;AACT;AA0IK,eAAgB,OAAO,SAAS,IAAI;AACzC,MAAI,QAAQ,KAAK,KAAK,QAAQ,KAAK;AAAG,WAAO;AAC7C,QAAM,eAAe,OAAO,OAAO,MAAM;AAC3C;AACA,MAAM,OAAO,MAAM;AA4EnB,qBAAsB,WAAW;AAC/B,iBAAgB,OAAc,SAAS,IAAI;AACzC,eAAW,UAAU,WAAW;AAC9B,UAAI;AACF,eAAO,OAAO,OAAO,MAAM;AAAA,MAC7B,SAAS,GAAP;AAAA,MAAW;AAAA,IACf;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,IAAI,UAAU,IAAI,QAAM,QAAQ,EAAE,CAAC,EAAE,KAAK,KAAK;AAClE,SAAO;AACT;AAGO,IAAM,UAAU;;;AC/YhB,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAC1B,IAAM,gBAAgB;AACtB,IAAM,uBAAuB;AAC7B,IAAM,oBAAoB;AAC1B,IAAM,oBAA4B,QAAQ,GAAG,CAAC,iBAAiB,mBAAmB,eAAe,sBAAsB,iBAAiB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAChK,IAAM,sBAAsB;AAC5B,IAAM,uBAAuB;AAC7B,IAAM,sBAAsB;AAC5B,IAAM,cAAsB,QAAQ,GAAG,CAAC,qBAAqB,sBAAsB,mBAAmB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;", "names": [] } diff --git a/test/contracts/shared/voting/proposals.js b/test/contracts/shared/voting/proposals.js index 4312b4f4d7..b4185abda4 100644 --- a/test/contracts/shared/voting/proposals.js +++ b/test/contracts/shared/voting/proposals.js @@ -332,14 +332,14 @@ var objectOf = (typeObj, _scope = "Object") => { } const undefAttr = typeAttrs.find((property) => { const propertyTypeFn = typeObj[property]; - return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); }); if (undefAttr) { throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); } const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { const typeFn = typeObj[key]; - if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { return Object.assign(acc, {}); } else { return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); @@ -348,7 +348,10 @@ var objectOf = (typeObj, _scope = "Object") => { return typeAttrs.reduce(reducer, {}); } object2.type = () => { - const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); return `{| ${props.join(",\n ")} |}`; diff --git a/test/contracts/shared/voting/proposals.js.map b/test/contracts/shared/voting/proposals.js.map index a595b17598..a791210663 100644 --- a/test/contracts/shared/voting/proposals.js.map +++ b/test/contracts/shared/voting/proposals.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../../../node_modules/@sbp/sbp/dist/module.mjs", "../../../../frontend/common/common.js", "../../../../frontend/common/vSafeHtml.js", "../../../../frontend/model/contracts/shared/giLodash.js", "../../../../frontend/common/translations.js", "../../../../frontend/common/stringTemplate.js", "../../../../frontend/model/contracts/misc/flowTyper.js", "../../../../frontend/model/contracts/shared/time.js", "../../../../frontend/model/contracts/shared/constants.js", "../../../../frontend/model/contracts/shared/voting/rules.js", "../../../../frontend/model/contracts/shared/voting/proposals.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n"], + "mappings": ";;;AAKA,IAAM,YAAY,CAAC;AACnB,IAAM,UAAU,CAAC;AACjB,IAAM,gBAAgB,CAAC;AACvB,IAAM,gBAAgB,CAAC;AACvB,IAAM,kBAAkB,CAAC;AACzB,IAAM,kBAAkB,CAAC;AAEzB,IAAM,eAAe;AAErB,aAAc,aAAa,MAAM;AAC/B,QAAM,SAAS,mBAAmB,QAAQ;AAC1C,MAAI,CAAC,UAAU,WAAW;AACxB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AAGA,aAAW,WAAW,CAAC,gBAAgB,WAAW,cAAc,SAAS,aAAa,GAAG;AACvF,QAAI,SAAS;AACX,iBAAW,UAAU,SAAS;AAC5B,YAAI,OAAO,QAAQ,UAAU,IAAI,MAAM;AAAO;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACA,SAAO,UAAU,UAAU,KAAK,QAAQ,QAAQ,OAAO,GAAG,IAAI;AAChE;AAEO,4BAA6B,UAAU;AAC5C,QAAM,eAAe,aAAa,KAAK,QAAQ;AAC/C,MAAI,iBAAiB,MAAM;AACzB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AACA,SAAO,aAAa;AACtB;AAEA,IAAM,qBAAqB;AAAA,EACzB,0BAA0B,SAAU,MAAM;AACxC,UAAM,aAAa,CAAC;AACpB,eAAW,YAAY,MAAM;AAC3B,YAAM,SAAS,mBAAmB,QAAQ;AAC1C,UAAI,UAAU,WAAW;AACvB,QAAC,SAAQ,QAAQ,QAAQ,KAAK,6DAA6D,WAAW;AAAA,MACxG,WAAW,OAAO,KAAK,cAAc,YAAY;AAC/C,YAAI,gBAAgB,WAAW;AAE7B,UAAC,SAAQ,QAAQ,QAAQ,KAAK,6CAA6C,gDAAgD;AAAA,QAC7H;AACA,cAAM,KAAK,UAAU,YAAY,KAAK;AACtC,mBAAW,KAAK,QAAQ;AAExB,YAAI,CAAC,QAAQ,SAAS;AACpB,kBAAQ,UAAU,EAAE,OAAO,CAAC,EAAE;AAAA,QAChC;AAEA,YAAI,aAAa,GAAG,gBAAgB;AAClC,aAAG,KAAK,QAAQ,QAAQ,KAAK;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,4BAA4B,SAAU,MAAM;AAC1C,eAAW,YAAY,MAAM;AAC3B,UAAI,CAAC,gBAAgB,WAAW;AAC9B,cAAM,IAAI,MAAM,0CAA0C,UAAU;AAAA,MACtE;AACA,aAAO,UAAU;AAAA,IACnB;AAAA,EACF;AAAA,EACA,2BAA2B,SAAU,MAAM;AACzC,QAAI,4BAA4B,OAAO,KAAK,IAAI,CAAC;AACjD,WAAO,IAAI,0BAA0B,IAAI;AAAA,EAC3C;AAAA,EACA,wBAAwB,SAAU,MAAM;AACtC,eAAW,YAAY,MAAM;AAC3B,UAAI,UAAU,WAAW;AACvB,cAAM,IAAI,MAAM,mDAAmD;AAAA,MACrE;AACA,sBAAgB,YAAY;AAAA,IAC9B;AAAA,EACF;AAAA,EACA,sBAAsB,SAAU,MAAM;AACpC,eAAW,YAAY,MAAM;AAC3B,aAAO,gBAAgB;AAAA,IACzB;AAAA,EACF;AAAA,EACA,oBAAoB,SAAU,KAAK;AACjC,WAAO,UAAU;AAAA,EACnB;AAAA,EACA,0BAA0B,SAAU,QAAQ;AAC1C,kBAAc,KAAK,MAAM;AAAA,EAC3B;AAAA,EACA,0BAA0B,SAAU,QAAQ,QAAQ;AAClD,QAAI,CAAC,cAAc;AAAS,oBAAc,UAAU,CAAC;AACrD,kBAAc,QAAQ,KAAK,MAAM;AAAA,EACnC;AAAA,EACA,4BAA4B,SAAU,UAAU,QAAQ;AACtD,QAAI,CAAC,gBAAgB;AAAW,sBAAgB,YAAY,CAAC;AAC7D,oBAAgB,UAAU,KAAK,MAAM;AAAA,EACvC;AACF;AAEA,mBAAmB,0BAA0B,kBAAkB;AAE/D,IAAO,iBAAQ;;;ACpFf;;;ACNA;AACA;;;ACwBO,mBAAoB,KAAK;AAC9B,SAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;AACvC;;;ADtBO,IAAM,gBAAgB;AAAA,EAC3B,cAAc,CAAC,OAAO;AAAA,EACtB,cAAc,CAAC,KAAK,MAAM,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EAEtF,qBAAqB;AACvB;AAEA,IAAM,YAAY,CAAC,IAAI,YAAY;AACjC,MAAI,QAAQ,aAAa,QAAQ,OAAO;AACtC,QAAI,SAAS;AACb,QAAI,QAAQ,QAAQ,KAAK;AACvB,eAAS,UAAU,MAAM;AACzB,aAAO,aAAa,KAAK,QAAQ,QAAQ;AACzC,aAAO,aAAa,KAAK,GAAG;AAAA,IAC9B;AACA,OAAG,cAAc;AACjB,OAAG,YAAY,UAAU,SAAS,QAAQ,OAAO,MAAM,CAAC;AAAA,EAC1D;AACF;AAWA,IAAI,UAAU,aAAa;AAAA,EACzB,MAAM;AAAA,EACN,QAAQ;AACV,CAAC;;;AElDD;AACA;;;ACNA,IAAM,QAAQ;AAEC,kBAAmB,WAAmB,MAAqB;AACxE,QAAM,WAAW,KAAK;AAGtB,QAAM,oBACH,OAAO,aAAa,YAAY,aAAa,OAC1C,WACA;AAGN,SAAO,OAAO,QAAQ,OAAO,oBAAqB,OAAO,SAAS,OAAO;AAEvE,QAAI,OAAO,QAAQ,OAAO,OAAO,OAAO,QAAQ,MAAM,YAAY,KAAK;AACrE,aAAO;AAAA,IACT;AAEA,UAAM,mBAGJ,OAAO,UAAU,eAAe,KAAK,mBAAmB,OAAO,IAC3D,kBAAkB,WAClB;AAGN,QAAI,qBAAqB,QAAQ,qBAAqB,QAAW;AAC/D,aAAO;AAAA,IACT;AACA,WAAO,OAAO,gBAAgB;AAAA,EAChC,CAAC;AACH;;;ADtBA,KAAI,UAAU,IAAI;AAClB,KAAI,UAAU,QAAQ;AAEtB,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,0BAAgD,CAAC;AAOvD,IAAM,kBAAkB;AAAA,EACtB,GAAG;AAAA,EACH,cAAc,CAAC,SAAS,QAAQ,OAAO,QAAQ;AAAA,EAC/C,cAAc,CAAC,KAAK,KAAK,MAAM,UAAU,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EACrG,qBAAqB;AACvB;AAEA,IAAI,kBAAkB;AACtB,IAAI,sBAAsB,gBAAgB,MAAM,GAAG,EAAE;AACrD,IAAI,0BAA0B;AAY9B,eAAI,0BAA0B;AAAA,EAC5B,qBAAqB,oBAAqB,UAAiC;AAEzE,UAAM,CAAC,gBAAgB,SAAS,YAAY,EAAE,MAAM,GAAG;AAGvD,QAAI,SAAS,YAAY,MAAM,gBAAgB,YAAY;AAAG;AAI9D,QAAI,iBAAiB;AAAqB;AAG1C,QAAI,iBAAiB,qBAAqB;AACxC,wBAAkB;AAClB,4BAAsB;AACtB,gCAA0B;AAC1B;AAAA,IACF;AACA,QAAI;AACF,gCAA2B,MAAM,eAAI,4BAA4B,QAAQ,KAAM;AAG/E,wBAAkB;AAClB,4BAAsB;AAAA,IACxB,SAAS,OAAP;AACA,cAAQ,MAAM,KAAK;AAAA,IACrB;AAAA,EACF;AACF,CAAC;AA0CM,kBAAmB,MAAiC;AACzD,QAAM,IAAI;AAAA,IACR,OAAO;AAAA,EACT;AACA,aAAW,OAAO,MAAM;AACtB,MAAE,GAAG,UAAU,IAAI;AACnB,MAAE,IAAI,SAAS,KAAK;AAAA,EACtB;AACA,SAAO;AACT;AAEe,WACb,KACA,MACQ;AACR,SAAO,SAAS,wBAAwB,QAAQ,KAAK,IAAI,EAEtD,QAAQ,iBAAiB,QAAQ;AACtC;AAgBA,kBAAmB,aAAa;AAC9B,SAAO,WAAU,SAAS,aAAa,eAAe;AACxD;AAEA,KAAI,UAAU,QAAQ;AAAA,EACpB,YAAY;AAAA,EACZ,OAAO;AAAA,IACL,MAAM,CAAC,QAAQ,KAAK;AAAA,IACpB,KAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA,SAAS;AAAA,EACX;AAAA,EACA,QAAQ,SAAU,GAAG,SAAS;AAC5B,UAAM,OAAO,QAAQ,SAAS,GAAG;AACjC,UAAM,cAAc,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,CAAC;AACpD,QAAI,CAAC,aAAa;AAChB,cAAQ,KAAK,yDAAyD,IAAI;AAC1E,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,IAAI;AAAA,IAChD;AAEA,QAAI,QAAQ,MAAM,QAAQ,OAAO,QAAQ,KAAK,MAAM,WAAW,UAAU;AACvE,cAAQ,KAAK,MAAM,MAAM;AAAA,IAC3B;AACA,QAAI,QAAQ,MAAM,SAAS;AACzB,YAAM,SAAS,KAAI,QAAQ,WAAW,SAAS,WAAW,IAAI,SAAS;AAEvE,aAAO,OAAO,OAAO,KAAK;AAAA,QACxB,IAAI,CAAC,QAAQ,SAAS;AACpB,cAAI,QAAQ,QAAQ;AAClB,mBAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,GAAG,IAAI;AAAA,UACnD,OAAO;AACL,mBAAO,EAAE,KAAK,GAAG,IAAI;AAAA,UACvB;AAAA,QACF;AAAA,QACA,IAAI,OAAK;AAAA,MACX,CAAC;AAAA,IACH,OAAO;AACL,UAAI,CAAC,QAAQ,KAAK;AAAU,gBAAQ,KAAK,WAAW,CAAC;AACrD,cAAQ,KAAK,SAAS,YAAY,SAAS,WAAW;AACtD,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,IAAI;AAAA,IAC1C;AAAA,EACF;AACF,CAAC;;;AE5IM,IAAM,cAAc,OAAO,SAAS;AACpC,IAAM,UAAU,OAAK,MAAM;AAC3B,IAAM,QAAQ,OAAK,MAAM;AACzB,IAAM,UAAU,OAAK,OAAO,MAAM;AAClC,IAAM,YAAY,OAAK,OAAO,MAAM;AACpC,IAAM,WAAW,OAAK,OAAO,MAAM;AAEnC,IAAM,WAAW,OAAK,CAAC,MAAM,CAAC,KAAK,OAAO,MAAM;AAChD,IAAM,aAAa,OAAK,OAAO,MAAM;AAcrC,IAAM,UAAU,CAAC,QAAQ,aAAa;AAC3C,MAAI,WAAW,OAAO,IAAI;AAAG,WAAO,OAAO,KAAK,QAAQ;AACxD,SAAO,OAAO,QAAQ;AACxB;AAGO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YACE,SACA,cACA,WACA,OACA,WAAmB,IACnB,YAAqB,IACrB;AACA,UAAM,aAAa,WACjB,YAAY,0BAA0B,YAAY;AACpD,UAAM,UAAU;AAChB,SAAK,eAAe;AACpB,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,YAAY,aAAa;AAC9B,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,UAAU,GAAG;AAAA,EAAe,KAAK,aAAa;AACnD,SAAK,OAAO,KAAK,YAAY;AAC7B,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,kBAAkB;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,gBAAyB;AACvB,UAAM,YAAY,KAAK,MAAM,MAAM,gCAAgC,KAAK,CAAC;AACzE,WAAO,UAAU,KAAK,cAAY,SAAS,QAAQ,qBAAqB,MAAM,EAAE,KAAK;AAAA,EACvF;AAAA,EAEA,eAAwB;AACtB,WAAO;AAAA,eACI,KAAK;AAAA,eACL,KAAK;AAAA,eACL,KAAK,aAAa,QAAQ,OAAO,EAAE;AAAA,eACnC,KAAK;AAAA,eACL,KAAK;AAAA;AAAA,EAElB;AACF;AAKA,IAAM,iBAAoB,CACxB,QACA,OACA,OACA,SACA,cACA,cACuB;AACvB,SAAO,IAAI,mBACT,SACA,gBAAgB,QAAQ,MAAM,GAC9B,aAAa,OAAO,OACpB,KAAK,UAAU,KAAK,GACpB,OAAO,MACP,KACF;AACF;AAgBO,IAAM,YACM,CAAC,cAAmC;AACnD,mBAAkB,OAAO,SAAS,IAAI;AACpC,QAAI,QAAQ,KAAK,KAAM,UAAU;AAAY,aAAO;AACpD,UAAM,eAAe,SAAS,OAAO,MAAM;AAAA,EAC7C;AACA,UAAQ,OAAO,MAAM;AACnB,QAAI,UAAU,SAAS;AAAG,aAAO,GAAG,YAAY,SAAS;AAAA;AACpD,aAAO,IAAI;AAAA,EAClB;AACA,SAAO;AACT;AAyCK,IAAM,SACX,SAAU,OAAO;AACf,MAAI,QAAQ,KAAK;AAAG,WAAO,CAAC;AAC5B,MAAI,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,KAAK,GAAG;AAC5C,WAAO,OAAO,OAAO,CAAC,GAAG,KAAK;AAAA,EAChC;AACA,QAAM,eAAe,QAAQ,KAAK;AACpC;AAGK,IAAM,WACX,CAAC,SAAY,SAAkB,aAAoE;AACnG,mBAAkB,OAAO;AACvB,UAAM,IAAI,OAAO,KAAK;AACtB,UAAM,YAAY,OAAO,KAAK,OAAO;AACrC,UAAM,cAAc,OAAO,KAAK,CAAC,EAAE,KAAK,UAAQ,CAAC,UAAU,SAAS,IAAI,CAAC;AACzE,QAAI,aAAa;AACf,YAAM,eACJ,SACA,OACA,QACA,4BAA4B,mBAAmB,aACjD;AAAA,IACF;AAIA,UAAM,YAAY,UAAU,KAAK,cAAY;AAC3C,YAAM,iBAAiB,QAAQ;AAC/B,aAAQ,eAAe,KAAK,SAAS,OAAO,KAAK,CAAC,EAAE,eAAe,QAAQ;AAAA,IAC7E,CAAC;AACD,QAAI,WAAW;AACb,YAAM,eACJ,SACA,EAAE,YACF,GAAG,UAAU,aACb,0BAA0B,kBAAkB,eAC5C,iBAAiB,QAAQ,QAAQ,UAAU,EAAE,OAAO,CAAC,KACrD,GACF;AAAA,IACF;AAEA,UAAM,UAAU,QAAQ,KAAK,IACzB,CAAC,KAAK,QAAQ,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,CAAC,IAC/D,CAAC,KAAK,QAAQ;AACd,YAAM,SAAS,QAAQ;AACvB,UAAI,OAAO,KAAK,SAAS,UAAU,KAAK,CAAC,EAAE,eAAe,GAAG,GAAG;AAC9D,eAAO,OAAO,OAAO,KAAK,CAAC,CAAC;AAAA,MAC9B,OAAO;AACL,eAAO,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,OAAO,EAAE,MAAM,GAAG,UAAU,KAAK,EAAE,CAAC;AAAA,MACzE;AAAA,IACF;AACF,WAAO,UAAU,OAAO,SAAS,CAAC,CAAC;AAAA,EACrC;AACA,UAAQ,OAAO,MAAM;AACnB,UAAM,QAAQ,OAAO,KAAK,OAAO,EAAE,IACjC,CAAC,QAAQ;AACP,YAAM,MAAM,QAAQ,KAAK,KAAK,SAAS,UAAU,IAC/C,GAAG,SAAS,QAAQ,QAAQ,MAAM,EAAE,QAAQ,KAAK,CAAC,MAClD,GAAG,QAAQ,QAAQ,QAAQ,IAAI;AACjC,aAAO;AAAA,IACT,CACF;AACA,WAAO;AAAA,GAAQ,MAAM,KAAK,OAAO;AAAA;AAAA,EACnC;AACA,SAAO;AACT;AA+BO,eAAgB,OAAO,SAAS,IAAI;AACzC,MAAI,QAAQ,KAAK,KAAK,QAAQ,KAAK;AAAG,WAAO;AAC7C,QAAM,eAAe,OAAO,OAAO,MAAM;AAC3C;AACA,MAAM,OAAO,MAAM;AAYZ,IAAM,SACX,iBAAiB,OAAO,SAAS,IAAI;AACnC,MAAI,QAAQ,KAAK;AAAG,WAAO;AAC3B,MAAI,SAAS,KAAK;AAAG,WAAO;AAC5B,QAAM,eAAe,SAAQ,OAAO,MAAM;AAC5C;AA2DF,qBAAsB,WAAW;AAC/B,iBAAgB,OAAc,SAAS,IAAI;AACzC,eAAW,UAAU,WAAW;AAC9B,UAAI;AACF,eAAO,OAAO,OAAO,MAAM;AAAA,MAC7B,SAAS,GAAP;AAAA,MAAW;AAAA,IACf;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,IAAI,UAAU,IAAI,QAAM,QAAQ,EAAE,CAAC,EAAE,KAAK,KAAK;AAClE,SAAO;AACT;AAGO,IAAM,UAAU;;;AC/YhB,IAAM,cAAc;AACpB,IAAM,eAAe,KAAK;AAC1B,IAAM,cAAc,KAAK;AACzB,IAAM,gBAAgB,KAAK;;;ACQ3B,IAAM,iBAAiB;AAAA,EAC5B,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AACX;AAEO,IAAM,kBAAkB;AACxB,IAAM,yBAAyB;AAC/B,IAAM,yBAAyB;AAC/B,IAAM,gCAAgC;AACtC,IAAM,mCAAmC;AACzC,IAAM,mBAAmB;AAMzB,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;;;ACzBtB,IAAM,eAAe;AACrB,IAAM,mBAAmB;AACzB,IAAM,iBAAiB;AACvB,IAAM,WAAW;AAEjB,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAC1B,IAAM,oBAAoB;AAejC,IAAM,gBAAgB,CAAC,UAAU,OAAO,KAAK,MAAM,QAAQ,EAAE,OAAO,OAAK,MAAM,SAAS,GAAG,WAAW,eAAe,MAAM,EAAE;AAE7H,IAAM,QAAgB;AAAA,EACpB,CAAC,kBAAkB,SAAU,OAAO,eAAc,OAAO;AACvD,YAAQ,OAAO,OAAO,KAAK;AAC3B,QAAI,aAAa,cAAc,KAAK;AACpC,QAAI,kBAAiB;AAAwB,oBAAc;AAC3D,UAAM,mBAAmB,MAAM,SAAS,UAAU,eAAc,aAAa,iBAAiB;AAC9F,UAAM,YAAY,qBAAqB,iBAAiB,kBAAkB,UAAU;AACpF,UAAM,mBAAmB,MAAM,OAAO,OAAK,MAAM,gBAAgB,EAAE;AACnE,UAAM,WAAW,MAAM,OAAO,OAAK,MAAM,QAAQ,EAAE;AACnD,UAAM,eAAe,MAAM,OAAO,OAAK,MAAM,YAAY,EAAE;AAC3D,UAAM,oBAAoB,WAAW;AACrC,UAAM,UAAU,oBAAoB;AACpC,UAAM,SAAS,aAAa;AAK5B,UAAM,eAAe,KAAK,KAAK,YAAa,cAAa,iBAAiB;AAC1E,YAAQ,MAAM,cAAc,uBAAuB,kBAAiB,EAAE,cAAc,UAAU,cAAc,kBAAkB,WAAW,QAAQ,SAAS,WAAW,CAAC;AACtK,QAAI,YAAY,cAAc;AAC5B,aAAO;AAAA,IACT;AACA,WAAO,WAAW,SAAS,eAAe,eAAe;AAAA,EAC3D;AAAA,EACA,CAAC,oBAAoB,SAAU,OAAO,eAAc,OAAO;AACzD,YAAQ,OAAO,OAAO,KAAK;AAC3B,UAAM,aAAa,cAAc,KAAK;AACtC,UAAM,aAAa,kBAAiB,yBAAyB,IAAI;AACjE,UAAM,oBAAoB,KAAK,IAAI,MAAM,SAAS,UAAU,eAAc,aAAa,mBAAmB,WAAW,UAAU;AAC/H,UAAM,YAAY,qBAAqB,mBAAmB,mBAAmB,UAAU;AACvF,UAAM,WAAW,MAAM,OAAO,OAAK,MAAM,QAAQ,EAAE;AACnD,UAAM,eAAe,MAAM,OAAO,OAAK,MAAM,YAAY,EAAE;AAC3D,UAAM,UAAU,MAAM;AACtB,UAAM,SAAS,aAAa;AAE5B,YAAQ,MAAM,cAAc,yBAAyB,kBAAiB,EAAE,UAAU,cAAc,WAAW,SAAS,YAAY,OAAO,CAAC;AACxI,QAAI,gBAAgB,WAAW;AAC7B,aAAO;AAAA,IACT;AAGA,WAAO,eAAe,SAAS,YAAY,WAAW;AAAA,EACxD;AAAA,EACA,CAAC,oBAAoB,SAAU,OAAO,eAAc,OAAO;AACzD,UAAM,IAAI,MAAM,gBAAgB;AAAA,EAMlC;AACF;AAEA,IAAO,gBAAQ;AAER,IAAM,WAAgB,QAAQ,GAAG,OAAO,KAAK,KAAK,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAc1E,IAAM,uBAAuB,CAAC,MAAc,WAAmB,cAA8B;AAClG,QAAM,kBAAkB,KAAK,IAAI,GAAG,SAAS;AAE7C,SAAO;AAAA,IACL,CAAC,oBAAoB,MAAM;AAEzB,aAAO,KAAK,IAAI,kBAAkB,GAAG,SAAS;AAAA,IAChD;AAAA,IACA,CAAC,kBAAkB,MAAM;AAEvB,YAAM,eAAe,IAAI;AACzB,aAAO,KAAK,IAAI,cAAc,SAAS;AAAA,IACzC;AAAA,EACF,EAAE,MAAM;AACV;;;AC9FO,yBAA0B,EAAE,OAAO,cAAc,UAAU,cAAc;AAC9E,WAAI,OAAO,MAAM,WAAW,YAAY;AACxC,iBAAI,qCAAqC,YACvC,CAAC,sCAAsC,YAAY,cAAc,QAAQ,CAC3E;AACF;AAEO,4BAA6B,SAAiB,cAA8B;AACjF,SAAO,GAAG,SAAS,2BAA2B,kBAAkB;AAClE;AAEO,IAAM,uBAA4B,SAAS;AAAA,EAChD,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,cAAc,SAAS;AAAA,IACrB,CAAC,kBAAkB,SAAS,EAAE,WAAW,OAAO,CAAC;AAAA,IACjD,CAAC,oBAAoB,SAAS,EAAE,WAAW,OAAO,CAAC;AAAA,EACrD,CAAC;AACH,CAAC;AAGM,uBAAwB,cAA+B;AAC5D,QAAM,YAAY,eAAI,kBAAkB;AACxC,QAAM,QAAQ,UAAU,UAAU;AAClC,QAAM,WAAW,MAAM,UAAU;AACjC,QAAM,QAAQ,OAAO,OAAO,CAAC,GAAG,SAAS,KAAK;AAC9C,QAAM,gBAAgB,cAAM,SAAS,KAAK,YAAY,OAAO,SAAS,KAAK,cAAc,KAAK;AAC9F,QAAM,OAAO,KAAK,OAAO,CAAC,KAAK;AAC/B,QAAM,YAAY,cAAM,SAAS,KAAK,YAAY,OAAO,SAAS,KAAK,cAAc,KAAK;AAC1F,UAAQ,MAAM,+BAA+B,4BAA4B,YAAY;AAErF,SAAO,kBAAkB,kBAAkB,cAAc;AAC3D;AAEA,qBAAsB,OAAY,EAAE,MAAM,MAAM,cAAmB;AACjE,QAAM,EAAE,iBAAiB;AACzB,QAAM,WAAW,MAAM,UAAU;AACjC,WAAS,SAAS;AAClB,iBAAI,yBAAyB,iBAAiB,OAAO,cAAc,IAAI;AACvE,kBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAC/D;AAIO,IAAM,mBAAmB;AAAA,EAC9B,MAAM;AAAA,EACN,YAAY,KAAK;AAAA,EACjB,cAAe;AAAA,IACb,CAAC,kBAAkB,EAAE,WAAW,KAAK;AAAA,IACrC,CAAC,oBAAoB,EAAE,WAAW,EAAE;AAAA,EACtC;AACF;AAEA,IAAM,YAAoB;AAAA,EACxB,CAAC,yBAAyB;AAAA,IACxB,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,UAAU,KAAK;AACxB,eAAS,SAAS;AAGlB,YAAM,UAAU,EAAE,MAAM,MAAM,KAAK,aAAa,WAAW;AAC3D,qBAAI,qCAAqC,SAAS,KAAK;AACvD,qBAAI,yBAAyB,iBAAiB,OAAO,UAAU,IAAI;AACnE,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IA+B/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,yBAAyB;AAAA,IACxB,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,cAAc,gBAAgB;AACtC,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,eAAS,UAAU;AACnB,YAAM,cAAc;AAAA,QAClB,GAAG,SAAS,KAAK;AAAA,QACjB;AAAA,QACA,iBAAiB;AAAA,MACnB;AACA,YAAM,UAAU,EAAE,MAAM,aAAa,MAAM,WAAW;AACtD,qBAAI,2CAA2C,SAAS,KAAK;AAC7D,qBAAI,qCAAqC,YACvC,CAAC,8CAA8C,OAAO,CACxD;AACA,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,gCAAgC;AAAA,IAC/B,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,YAAM,EAAE,SAAS,kBAAkB,SAAS,KAAK;AAGjD,YAAM,UAAU;AAAA,QACd;AAAA,QACA,MAAM,EAAE,CAAC,UAAU,cAAc;AAAA,QACjC;AAAA,MACF;AACA,qBAAI,6CAA6C,SAAS,KAAK;AAC/D,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,mCAAmC;AAAA,IAClC,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,YAAM,UAAU;AAAA,QACd;AAAA,QACA,MAAM,SAAS,KAAK;AAAA,QACpB;AAAA,MACF;AACA,qBAAI,mDAAmD,SAAS,KAAK;AACrE,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,mBAAmB;AAAA,IAClB,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,qBAAI,yBAAyB,iBAAiB,OAAO,UAAU,IAAI;AACnE,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AACF;AAEA,IAAO,oBAAQ;AAER,IAAM,eAAoB,QAAQ,GAAG,OAAO,KAAK,SAAS,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;", "names": [] } From 1af98c98f3e89663e25fbea3f77b3b843227859e Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Tue, 20 Sep 2022 20:19:52 +0200 Subject: [PATCH 23/43] Adding typescript linting --- Gruntfile.js | 2 +- backend/.eslintrc.json | 9 +- backend/auth.ts | 2 +- backend/database.ts | 10 +- backend/index.ts | 11 +- backend/pubsub.ts | 146 ++++--- backend/routes.ts | 12 +- backend/server.ts | 5 +- backend/types.ts | 43 +- frontend/.eslintrc.json | 32 -- frontend/controller/namespace.js | 2 +- package.json | 1 + shared/.eslintrc.json | 23 ++ shared/domains/chelonia/GIMessage.ts | 54 +-- shared/domains/chelonia/chelonia.ts | 205 ++++++---- shared/domains/chelonia/db.ts | 16 +- shared/domains/chelonia/errors.ts | 1 + shared/domains/chelonia/internals.ts | 72 +++- shared/functions.ts | 17 +- shared/pubsub.test.ts | 15 +- shared/pubsub.ts | 112 +++-- test/backend.test.js | 386 ------------------ test/backend.test.ts | 3 +- test/contracts/chatroom.js | 9 +- test/contracts/chatroom.js.map | 4 +- test/contracts/group.js | 10 +- test/contracts/group.js.map | 4 +- test/contracts/identity.js | 9 +- test/contracts/identity.js.map | 4 +- test/contracts/mailbox.js | 9 +- test/contracts/mailbox.js.map | 4 +- test/contracts/shared/payments/index.js.map | 4 +- test/contracts/shared/voting/proposals.js | 9 +- test/contracts/shared/voting/proposals.js.map | 4 +- 34 files changed, 499 insertions(+), 750 deletions(-) delete mode 100644 frontend/.eslintrc.json create mode 100644 shared/.eslintrc.json delete mode 100644 test/backend.test.js diff --git a/Gruntfile.js b/Gruntfile.js index 4860da3e06..08c381fcf5 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -397,7 +397,7 @@ module.exports = (grunt) => { options: { env: process.env } }, // Test anything in /test that ends with `.test.ts`. - testWithDeno: 'deno test --allow-env --allow-net --allow-read --allow-write --importmap=import-map.json --no-check ./test/*.ts', + testWithDeno: 'deno test --allow-env --allow-net --allow-read --allow-write --import-map=import-map.json --no-check ./test/*.ts', chelDeployAll: 'find contracts -iname "*.manifest.json" | xargs -r ./node_modules/.bin/chel deploy ./data' } }) diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json index 73ae078d61..bb4118c811 100644 --- a/backend/.eslintrc.json +++ b/backend/.eslintrc.json @@ -8,13 +8,16 @@ "import" ], "rules": { + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-this-alias": "off", "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], - "require-await": "error", - "quote-props": "off", "dot-notation": "off", "import/extensions": [ 2, "ignorePackages" - ] + ], + "no-use-before-define": "off", + "quote-props": "off", + "require-await": "error" } } diff --git a/backend/auth.ts b/backend/auth.ts index 2fff7f6ca5..78a94df3e6 100644 --- a/backend/auth.ts +++ b/backend/auth.ts @@ -9,7 +9,7 @@ const { PermissionDenied } = Deno.errors export default { name: 'gi-auth', - register (server: Object, opts: Object) { + register (server: unknown, opts: unknown) { server.auth.scheme('gi-auth', function (server, options) { return { authenticate (request, h) { diff --git a/backend/database.ts b/backend/database.ts index c2342f68dc..da6af19ea4 100644 --- a/backend/database.ts +++ b/backend/database.ts @@ -57,7 +57,7 @@ export default sbp('sbp/selectors/register', { chunks.push(']') break } else { - currentHEAD = entry.message().previousHEAD + currentHEAD = entry.message().previousHEAD } } } catch (error) { @@ -67,7 +67,7 @@ export default sbp('sbp/selectors/register', { }, 'backend/db/streamEntriesBefore': async function (before: string, limit: number): Promise { let currentHEAD = before - let entry = await sbp('chelonia/db/getEntry', currentHEAD) + const entry = await sbp('chelonia/db/getEntry', currentHEAD) if (!entry) { throw new NotFound(`entry ${currentHEAD} doesn't exist!`) } @@ -100,7 +100,7 @@ export default sbp('sbp/selectors/register', { 'backend/db/streamEntriesBetween': async function (startHash: string, endHash: string, offset: number): Promise { let isMet = false let currentHEAD = endHash - let entry = await sbp('chelonia/db/getEntry', currentHEAD) + const entry = await sbp('chelonia/db/getEntry', currentHEAD) if (!entry) { throw new NotFound(`entry ${currentHEAD} doesn't exist!`) } @@ -172,14 +172,14 @@ export default sbp('sbp/selectors/register', { } return await readFileAsync(filepath) }, - 'backend/db/writeFile': async function (filename: string, data: any) { + 'backend/db/writeFile': async function (filename: string, data: Uint8Array) { // TODO: check for how much space we have, and have a server setting // that determines how much of the disk space we're allowed to // use. If the size of the file would cause us to exceed this // amount, throw an exception return await writeFileAsync(throwIfFileOutsideDataDir(filename), data) }, - 'backend/db/writeFileOnce': async function (filename: string, data: any) { + 'backend/db/writeFileOnce': async function (filename: string, data: Uint8Array) { const filepath = throwIfFileOutsideDataDir(filename) if (await fileExists(filepath)) { console.warn('writeFileOnce: exists:', filepath) diff --git a/backend/index.ts b/backend/index.ts index 9b811baa03..d093507a76 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -1,8 +1,6 @@ -declare var process: any - import { bold } from 'fmt/colors.ts' -import sbp from '@sbp/sbp' +import sbp from '@sbp/sbp' import '@sbp/okturtles.data' import '@sbp/okturtles.events' import { notFound } from 'pogo/lib/bang.ts' @@ -12,7 +10,7 @@ import { SERVER_RUNNING } from './events.ts' import { PUBSUB_INSTANCE } from './instance-keys.ts' import type { PubsubClient, PubsubServer } from './pubsub.ts' -// @ts-ignore +// @ts-expect-error TS7017 [ERROR]: Element implicitly has an 'any' type. globalThis.logger = function (err: Error) { console.error(err) err.stack && console.error(err.stack) @@ -25,7 +23,7 @@ globalThis.logger = function (err: Error) { const dontLog: Record = { 'backend/server/broadcastEntry': true } -function logSBP (domain: string, selector: string, data: any) { +function logSBP (domain: string, selector: string, data: unknown) { if (!(selector in dontLog)) { console.log(bold(`[sbp] ${selector}`), data) } @@ -67,7 +65,8 @@ const shutdownFn = function () { }) } -Deno.addSignalListener('SIGUSR2', shutdownFn) +// Deno.addSignalListener('SIGBREAK', shutdownFn) +Deno.addSignalListener('SIGINT', shutdownFn) // Equivalent to the `uncaughtException` event in Nodejs. addEventListener('error', (event) => { diff --git a/backend/pubsub.ts b/backend/pubsub.ts index 8a207cd21a..5c93d9f40d 100644 --- a/backend/pubsub.ts +++ b/backend/pubsub.ts @@ -1,33 +1,30 @@ +import { messageParser } from '~/shared/pubsub.ts' + +/* eslint-disable @typescript-eslint/no-explicit-any */ +type Callback = (...args: any[]) => void + type JSONType = ReturnType -type Message = { - [key: string]: JSONType, +interface Message { + [key: string]: JSONType type: string } -type SubMessage = { - contractID: string - dontBroadcast?: boolean -} - -type UnsubMessage = { - contractID: string - dontBroadcast?: boolean -} +type MessageHandler = (this: PubsubServer, msg: Message) => void type PubsubClientEventName = 'close' | 'message' type PubsubServerEventName = 'close' | 'connection' | 'error' | 'headers' | 'listening' -import { - acceptWebSocket, - isWebSocketCloseEvent, - isWebSocketPingEvent, -} from 'https://deno.land/std@0.92.0/ws/mod.ts' - -import { messageParser } from '~/shared/pubsub.ts' - -const CI = Deno.env.get('CI') -const NODE_ENV = Deno.env.get('NODE_ENV') ?? 'development' +type PubsubServerOptions = { + clientHandlers?: Record + logPingRounds?: boolean + logPongMessages?: boolean + maxPayload?: number + messageHandlers?: Record + pingInterval?: number + rawHttpServer?: unknown + serverHandlers?: Record +} const emptySet = Object.freeze(new Set()) // Used to tag console output. @@ -35,8 +32,6 @@ const tag = '[pubsub]' // ====== Helpers ====== // -// Only necessary when using the `ws` module in Deno. - const generateSocketID = (() => { let counter = 0 @@ -46,12 +41,14 @@ const generateSocketID = (() => { const logger = { log: console.log.bind(console, tag), debug: console.debug.bind(console, tag), - error: console.error.bind(console, tag) + error: console.error.bind(console, tag), + info: console.info.bind(console, tag), + warn: console.warn.bind(console, tag) } // ====== API ====== // -export function createErrorResponse (data: Object): string { +export function createErrorResponse (data: JSONType): string { return JSON.stringify({ type: 'error', data }) } @@ -59,17 +56,17 @@ export function createMessage (type: string, data: JSONType): string { return JSON.stringify({ type, data }) } -export function createNotification (type: string, data: Object): string { +export function createNotification (type: string, data: JSONType): string { return JSON.stringify({ type, data }) } -export function createResponse (type: string, data: Object): string { +export function createResponse (type: string, data: JSONType): string { return JSON.stringify({ type, data }) } export class PubsubClient { - id: string activeSinceLastPing: boolean + id: string pinged: boolean server: PubsubServer socket: WebSocket @@ -94,7 +91,7 @@ export class PubsubClient { this.terminate() } - send (data: string | ArrayBufferLike | Blob | ArrayBufferView): void { + send (data: string | ArrayBufferLike | ArrayBufferView | Blob): void { const { socket } = this if (socket.readyState === WebSocket.OPEN) { this.socket.send(data) @@ -105,12 +102,12 @@ export class PubsubClient { terminate () { const { server, socket } = this - internalClientEventHandlers.close.call(this, 1000, '') + internalClientEventHandlers.close.call(this, new CloseEvent('close', { code: 4001, reason: 'terminated' })) // Remove listeners for socket events, i.e. events emitted on a socket object. ;['close', 'error', 'message', 'ping', 'pong'].forEach((eventName: string) => { socket.removeEventListener(eventName, internalClientEventHandlers[eventName as PubsubClientEventName] as EventListener) - if (typeof server.customClientEventHandlers[eventName] === 'function') { - socket.removeEventListener(eventName as keyof WebSocketEventMap, server.customClientEventHandlers[eventName] as EventListener) + if (typeof server.customClientHandlers[eventName] === 'function') { + socket.removeEventListener(eventName as keyof WebSocketEventMap, server.customClientHandlers[eventName] as EventListener) } }) socket.close() @@ -119,19 +116,19 @@ export class PubsubClient { export class PubsubServer { clients: Set - customServerEventHandlers: Record - customClientEventHandlers: Record - messageHandlers: Record - options: any + customClientHandlers: Record + customServerHandlers: Record + messageHandlers: Record + options: typeof defaultOptions pingIntervalID?: number - queuesByEventName: Map> + queuesByEventName: Map> subscribersByContractID: Record> - constructor (options: Object = {}) { + constructor (options: PubsubServerOptions = {}) { this.clients = new Set() - this.customServerEventHandlers = Object.create(null) - this.customClientEventHandlers = Object.create(null) - this.messageHandlers = { ...defaultMessageHandlers, ...(options as any).customMessageHandlers } + this.customClientHandlers = options.clientHandlers ?? Object.create(null) + this.customServerHandlers = options.serverHandlers ?? Object.create(null) + this.messageHandlers = { ...defaultMessageHandlers, ...options.messageHandlers } this.options = { ...defaultOptions, ...options } this.queuesByEventName = new Map() this.subscribersByContractID = Object.create(null) @@ -139,24 +136,25 @@ export class PubsubServer { close () { this.clients.forEach(client => client.terminate()) + this.emit('close') } handleUpgradeableRequest (request: Request): Response { const server = this const { socket, response } = Deno.upgradeWebSocket(request) - // Our code + socket.onopen = () => { server.emit('connection', socket, request) } return response } - emit (name: string, ...args: any[]) { + emit (name: string, ...args: unknown[]) { const server = this const queue = server.queuesByEventName.get(name) ?? emptySet try { for (const callback of queue) { - Function.prototype.call.call(callback as Function, server, ...args) + Function.prototype.call.call(callback as Callback, server, ...args) } } catch (error) { if (server.queuesByEventName.has('error')) { @@ -167,13 +165,13 @@ export class PubsubServer { } } - off (name: string, callback: Function) { + off (name: string, callback: Callback) { const server = this const queue = server.queuesByEventName.get(name) ?? emptySet queue.delete(callback) } - on (name: string, callback: Function) { + on (name: string, callback: Callback) { const server = this if (!server.queuesByEventName.has(name)) { server.queuesByEventName.set(name, new Set()) @@ -182,10 +180,6 @@ export class PubsubServer { queue?.add(callback) } - get port () { - return this.options.rawHttpServer?.listener?.addr?.port - } - /** * Broadcasts a message, ignoring clients which are not open. * @@ -214,17 +208,18 @@ export class PubsubServer { } } -export function createServer(options = {}) { +export function createServer (options: PubsubServerOptions = {}) { const server = new PubsubServer(options) // Add listeners for server events, i.e. events emitted on the server object. - Object.keys(internalServerHandlers).forEach((name: string) => { - server.on(name, (...args: any[]) => { + Object.keys(internalServerEventHandlers).forEach((name: string) => { + server.on(name, (...args: unknown[]) => { try { // Always call the default handler first. - // @ts-ignore TS2556 [ERROR]: A spread argument must either have a tuple type or be passed to a rest parameter. - internalServerHandlers[name as PubsubServerEventName]?.call(server, ...args) - server.customServerEventHandlers[name as PubsubServerEventName]?.call(server, ...args) + // @ts-expect-error TS2556: A spread argument must either have a tuple type or be passed to a rest parameter. + internalServerEventHandlers[name as PubsubServerEventName]?.call(server, ...args) + // @ts-expect-error TS2556: A spread argument must either have a tuple type or be passed to a rest parameter. + server.customServerHandlers[name as PubsubServerEventName]?.call(server, ...args) } catch (error) { server.emit('error', error) } @@ -255,20 +250,13 @@ export function createServer(options = {}) { export function isUpgradeableRequest (request: Request): boolean { const upgrade = request.headers.get('upgrade') - if (upgrade?.toLowerCase() === "websocket") return true + if (upgrade?.toLowerCase() === 'websocket') return true return false } -const defaultOptions = { - logPingRounds: true, - logPongMessages: true, - maxPayload: 6 * 1024 * 1024, - pingInterval: 30000 -} - // Internal default handlers for server events. // The `this` binding refers to the server object. -const internalServerHandlers = Object.freeze({ +const internalServerEventHandlers = { close () { logger.log('Server closed') }, @@ -285,7 +273,7 @@ const internalServerHandlers = Object.freeze({ const url = request.url const urlSearch = url.includes('?') ? url.slice(url.lastIndexOf('?')) : '' const debugID = new URLSearchParams(urlSearch).get('debugID') || '' - + const client = new PubsubClient(socket, server) client.id = generateSocketID(debugID) client.activeSinceLastPing = true @@ -301,9 +289,9 @@ const internalServerHandlers = Object.freeze({ logger.log(`Event '${eventName}' on client ${client.id}`, ...args.map(arg => String(arg))) } try { - // @ts-ignore TS2554 [ERROR]: Expected 3 arguments, but got 2. - (internalClientEventHandlers)[eventName as PubsubClientEventName]?.call(client, ...args) - server.customClientEventHandlers[eventName]?.call(client, ...args) + // @ts-expect-error TS2554 [ERROR]: Expected 3 arguments, but got 2. + internalClientEventHandlers[eventName as PubsubClientEventName]?.call(client, ...args) + server.customClientHandlers[eventName]?.call(client, ...args) } catch (error) { server.emit('error', error) client.terminate() @@ -319,14 +307,14 @@ const internalServerHandlers = Object.freeze({ listening () { logger.log('Server listening') } -}) +} // Default handlers for server-side client socket events. // The `this` binding refers to the connected `ws` socket object. const internalClientEventHandlers = Object.freeze({ - close (this: PubsubClient, code: number, reason: string) { + close (this: PubsubClient, event: CloseEvent) { const client = this - const { server, socket, id: socketID } = this + const { server, id: socketID } = this // Notify other clients that this one has left any room they shared. for (const contractID of client.subscriptions) { @@ -343,8 +331,8 @@ const internalClientEventHandlers = Object.freeze({ message (this: PubsubClient, event: MessageEvent) { const client = this - const { server, socket } = this - const { type, data } = event + const { server } = this + const { data } = event const text = data let msg: Message = { type: '' } @@ -403,7 +391,7 @@ const defaultMessageHandlers = { // Currently unused. }, - [SUB] (this: PubsubClient, { contractID, dontBroadcast }: SubMessage) { + [SUB] (this: PubsubClient, { contractID, dontBroadcast }: Message) { const client = this const { server, socket, id: socketID } = this @@ -427,7 +415,7 @@ const defaultMessageHandlers = { socket.send(createResponse(SUCCESS, { type: SUB, contractID })) }, - [UNSUB] (this: PubsubClient, { contractID, dontBroadcast }: UnsubMessage) { + [UNSUB] (this: PubsubClient, { contractID, dontBroadcast }: Message) { const client = this const { server, socket, id: socketID } = this @@ -451,3 +439,9 @@ const defaultMessageHandlers = { } } +const defaultOptions = { + logPingRounds: true, + logPongMessages: true, + maxPayload: 6 * 1024 * 1024, + pingInterval: 30000 +} diff --git a/backend/routes.ts b/backend/routes.ts index 2f7c0fedef..148e1d7cb6 100644 --- a/backend/routes.ts +++ b/backend/routes.ts @@ -1,5 +1,3 @@ -/* globals Deno */ - import { bold, yellow } from 'fmt/colors.ts' import sbp from '@sbp/sbp' @@ -15,13 +13,15 @@ import Toolkit from 'pogo/lib/toolkit.ts' import './database.ts' import * as pathlib from 'path' -declare const logger: Function +declare const logger: (err: Error) => Error export const router = new Router() -const route: Record = new Proxy({}, { - get: function (obj: any, prop: string) { - return function (path: string, handler: RouteHandler) { +type RouterProxyTarget = Record Response> + +const route = new Proxy({} as RouterProxyTarget, { + get (obj: RouterProxyTarget, prop: string) { + return (path: string, handler: RouteHandler) => { router.add({ path, method: prop, handler }) } } diff --git a/backend/server.ts b/backend/server.ts index 7d952035d7..bdae4af98b 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -18,9 +18,8 @@ import { router } from './routes.ts' import { GIMessage } from '../shared/domains/chelonia/GIMessage.ts' -const { default: { version } } = await import('~/package.json', { - assert: { type: 'json' } -}) +import packageJSON from '~/package.json' assert { type: 'json' } +const { version } = packageJSON const applyPortShift = (env: ReturnType) => { // TODO: implement automatic port selection when `PORT_SHIFT` is 'auto'. diff --git a/backend/types.ts b/backend/types.ts index c1c52e10bb..b23cf32c39 100644 --- a/backend/types.ts +++ b/backend/types.ts @@ -2,35 +2,36 @@ import Request from './request.ts' import ServerResponse from './response.ts' import Toolkit from './toolkit.ts' -export interface Route { - method: string, - path: string, - handler: RouteHandler, - vhost?: string -} - -export type RequestParams = { [param: string]: string } -export type RequestState = { [name: string]: string } +type JSONStringifyable = boolean | null | number | object | string -export interface RouteOptions extends Omit, 'method' | 'path'> { - method?: Route['method'] | Iterable, - path?: Route['path'] | Iterable +export interface MatchedRoute extends NormalizedRoute { + params: RequestParams } export interface NormalizedRoute extends Route { - paramNames: Array, - segments: Array + paramNames: Array, + segments: Array } -export interface MatchedRoute extends NormalizedRoute { - params: RequestParams -} +export type RequestParams = { [param: string]: string } +export type RequestState = { [name: string]: string } -type JSONStringifyable = boolean | null | number | object | string export type ResponseBody = Deno.Reader | Uint8Array | JSONStringifyable -export type RouteHandlerResult = ServerResponse | ResponseBody | Error | Promise + +export interface Route { + method: string, + path: string, + handler: RouteHandler, + vhost?: string +} export type RouteHandler = (request: Request, h: Toolkit) => RouteHandlerResult +export type RouteHandlerResult = ServerResponse | ResponseBody | Error | Promise + +export interface RouteOptions extends Omit, 'method' | 'path'> { + method?: Route['method'] | Iterable, + path?: Route['path'] | Iterable +} -export type { ServerOptions } from './server.ts' -export type { FileHandlerOptions } from './helpers/file.ts' export type { DirectoryHandlerOptions } from './helpers/directory.tsx' +export type { FileHandlerOptions } from './helpers/file.ts' +export type { ServerOptions } from './server.ts' diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json deleted file mode 100644 index 08d6073831..0000000000 --- a/frontend/.eslintrc.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "parserOptions": { - "parser": "@babel/eslint-parser" - }, - "extends": [ - "plugin:cypress/recommended", - "plugin:flowtype/recommended", - "plugin:vue/essential", - "standard" - ], - "plugins": [ - "cypress", - "flowtype", - "import" - ], - "ignorePatterns": [ - "assets/*", - "model/contracts/misc/flowTyper.js" - ], - "rules": { - "require-await": "error", - "vue/max-attributes-per-line": "off", - "vue/html-indent": "off", - "flowtype/no-types-missing-file-annotation": "off", - "quote-props": "off", - "dot-notation": "off", - "import/extensions": [ - 2, - "ignorePackages" - ] - } -} diff --git a/frontend/controller/namespace.js b/frontend/controller/namespace.js index 92ce12e4e8..a6bd1b5d67 100644 --- a/frontend/controller/namespace.js +++ b/frontend/controller/namespace.js @@ -26,7 +26,7 @@ sbp('sbp/selectors/register', { return cache[name] } return fetch(`${sbp('okTurtles.data/get', 'API_URL')}/name/${name}`).then((r) => { - if (!r.ok) { + if (!r.ok) { console.warn(`namespace/lookup: ${r.status} for ${name}`) if (r.status !== 404) { throw new Error(`${r.status}: ${r.statusText}`) diff --git a/package.json b/package.json index dba4a138aa..7e9d368163 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "eslintIgnore": [ "frontend/assets/*", "frontend/model/contracts/misc/flowTyper.js", + "shared/declarations.js", "historical/*", "shared/types.js", "dist/*", diff --git a/shared/.eslintrc.json b/shared/.eslintrc.json new file mode 100644 index 0000000000..5624761085 --- /dev/null +++ b/shared/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "parserOptions": { + "parser": "@typescript-eslint/parser" + }, + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "plugins": [ + "@typescript-eslint", + "import" + ], + "rules": { + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "dot-notation": "off", + "import/extensions": [ + 2, + "ignorePackages" + ], + "no-use-before-define": "off", + "quote-props": "off", + "require-await": "error" + } +} diff --git a/shared/domains/chelonia/GIMessage.ts b/shared/domains/chelonia/GIMessage.ts index 6180cbab18..940c67c29f 100644 --- a/shared/domains/chelonia/GIMessage.ts +++ b/shared/domains/chelonia/GIMessage.ts @@ -6,27 +6,35 @@ import type { JSONObject } from '~/shared/types.ts' type JSONType = ReturnType type Mapping = { - key: string; - value: string; + key: string + value: string } type Message = { - version: string; // Semver version string - previousHEAD: string | null; - contractID: string | null; - op: GIOp - manifest: string; + contractID: string | null + manifest: string // The nonce makes it difficult to predict message contents // and makes it easier to prevent conflicts during development. - nonce: number; + nonce: number + op: GIOp + previousHEAD: string | null + version: string // Semver version string } +type Signature = { + sig: string + type: string +} + +type DecryptFunction = (v: GIOpActionEncrypted) => GIOpActionUnencrypted +type SignatureFunction = (data: string) => Signature + export type GIKeyType = '' export type GIKey = { - type: GIKeyType; - data: Object; // based on GIKeyType this will change - meta: Object; + type: GIKeyType + data: JSONType // based on GIKeyType this will change + meta: JSONObject } // Allows server to check if the user is allowed to register this type of contract // TODO: rename 'type' to 'contractName': @@ -45,14 +53,14 @@ export class GIMessage { _mapping: Mapping _message: Message - static OP_CONTRACT: 'c' = 'c' - static OP_ACTION_ENCRYPTED: 'ae' = 'ae' // e2e-encrypted action - static OP_ACTION_UNENCRYPTED: 'au' = 'au' // publicly readable action - static OP_KEY_ADD: 'ka' = 'ka' // add this key to the list of keys allowed to write to this contract, or update an existing key - static OP_KEY_DEL: 'kd' = 'kd' // remove this key from authorized keys - static OP_PROTOCOL_UPGRADE: 'pu' = 'pu' - static OP_PROP_SET: 'ps' = 'ps' // set a public key/value pair - static OP_PROP_DEL: 'pd' = 'pd' // delete a public key/value pair + static OP_CONTRACT = 'c' as const + static OP_ACTION_ENCRYPTED = 'ae' as const // e2e-encrypted action + static OP_ACTION_UNENCRYPTED = 'au' as const // publicly readable action + static OP_KEY_ADD = 'ka' as const // add this key to the list of keys allowed to write to this contract, or update an existing key + static OP_KEY_DEL = 'kd' as const // remove this key from authorized keys + static OP_PROTOCOL_UPGRADE = 'pu' as const + static OP_PROP_SET = 'ps' as const // set a public key/value pair + static OP_PROP_DEL = 'pd' as const // delete a public key/value pair // eslint-disable-next-line camelcase static createV1_0 ( @@ -60,7 +68,7 @@ export class GIMessage { previousHEAD: string | null = null, op: GIOp, manifest: string, - signatureFn: Function = defaultSignatureFn + signatureFn: SignatureFunction = defaultSignatureFn ): GIMessage { const message: Message = { version: '1.0.0', @@ -113,11 +121,11 @@ export class GIMessage { } } - decryptedValue (fn?: Function): any { + decryptedValue (fn?: DecryptFunction): GIOpValue { if (!this._decrypted) { this._decrypted = ( this.opType() === GIMessage.OP_ACTION_ENCRYPTED && fn !== undefined - ? fn(this.opValue()) + ? fn(this.opValue() as string) : this.opValue() ) } @@ -160,7 +168,7 @@ export class GIMessage { hash (): string { return this._mapping.key } } -function defaultSignatureFn (data: string) { +function defaultSignatureFn (data: string): Signature { return { type: 'default', sig: blake32Hash(data) diff --git a/shared/domains/chelonia/chelonia.ts b/shared/domains/chelonia/chelonia.ts index 33e56b35fc..924f420e6d 100644 --- a/shared/domains/chelonia/chelonia.ts +++ b/shared/domains/chelonia/chelonia.ts @@ -1,5 +1,4 @@ -declare var process: any - +/* eslint-disable camelcase */ import sbp from '@sbp/sbp' import '@sbp/okturtles.events' import '@sbp/okturtles.eventqueue' @@ -12,10 +11,38 @@ import { handleFetchResult } from '~/frontend/controller/utils/misc.js' // TODO: rename this to ChelMessage import { GIMessage } from './GIMessage.ts' import { ChelErrorUnrecoverable } from './errors.ts' -import type { GIOpContract, GIOpActionUnencrypted } from './GIMessage.ts' +import type { GIOpActionUnencrypted } from './GIMessage.ts' +declare const process: { + env: Record +} type JSONType = ReturnType +export type ChelActionParams = { + action: string; + server?: string; // TODO: implement! + contractID: string; + data: JSONType; + hooks?: { + prepublishContract?: (msg: GIMessage) => void; + prepublish?: (msg: GIMessage) => void; + postpublish?: (msg: GIMessage) => void; + }; + publishOptions?: { maxAttempts: number }; +} + +export type ChelRegParams = { + contractName: string; + server?: string; // TODO: implement! + data: JSONType; + hooks?: { + prepublishContract?: (msg: GIMessage) => void; + prepublish?: (msg: GIMessage) => void; + postpublish?: (msg: GIMessage) => void; + }; + publishOptions?: { maxAttempts: number }; +} + export type CheloniaConfig = { connectionOptions: { maxRetries: number @@ -25,14 +52,14 @@ export type CheloniaConfig = { connectionURL: null | string contracts: { defaults: { - modules: Record + modules: Record exposedGlobals: Record allowedDomains: string[] allowedSelectors: string[] preferSlim: boolean } manifests: Record // Contract names -> manifest hashes - overrides: Record // Override default values per-contract. + overrides: Record // Override default values per-contract. } decryptFn: (arg: string) => JSONType encryptFn: (arg: JSONType) => string @@ -45,10 +72,26 @@ export type CheloniaConfig = { syncContractError: null | ((e: Error, contractID: string) => void) pubsubError: null | ((e: Error, socket: WebSocket) => void) } - postOp?: (state: any, message: any) => boolean - preOp?: (state: any, message: any) => boolean - reactiveDel: (obj: any, key: string) => void - reactiveSet: (obj: any, key: string, value: any) => typeof value + postOp?: (state: unknown, message: unknown) => boolean + postOp_ae?: PostOp + postOp_au?: PostOp + postOp_c?: PostOp + postOp_ka?: PostOp + postOp_kd?: PostOp + postOp_pd?: PostOp + postOp_ps?: PostOp + postOp_pu?: PostOp + preOp?: PreOp + preOp_ae?: PreOp + preOp_au?: PreOp + preOp_c?: PreOp + preOp_ka?: PreOp + preOp_kd?: PreOp + preOp_pd?: PreOp + preOp_ps?: PreOp + preOp_pu?: PreOp + reactiveDel: (obj: Record, key: string) => void + reactiveSet: (obj: Record, key: string, value: unknown) => typeof value skipActionProcessing: boolean skipSideEffects: boolean skipProcessing?: boolean @@ -59,68 +102,86 @@ export type CheloniaConfig = { export type CheloniaInstance = { config: CheloniaConfig; contractsModifiedListener?: () => void; - contractSBP?: any; - defContract?: ContractDefinition; + contractSBP?: unknown; + defContract: ContractDefinition; defContractManifest?: string; defContractSBP?: SBP; defContractSelectors?: string[]; - manifestToContract: Record; - sideEffectStack: (contractID: string) => any[]; - sideEffectStacks: Record; + manifestToContract: Record + sideEffectStack: (contractID: string) => SBPCallParameters[]; + sideEffectStacks: Record; state: CheloniaState; - whitelistedActions: Record; + whitelistedActions: Record; } -export type CheloniaState = { - contracts: Record; // Contracts we've subscribed to. - pending: string[]; // Prevents processing unexpected data from a malicious server. +export interface CheloniaState { + [contractID: string]: unknown + contracts: Record // contractIDs => { type:string, HEAD:string } (for contracts we've successfully subscribed to) + pending: string[] // Prevents processing unexpected data from a malicious server. } +type Action = { + validate: (data: JSONType, { state, getters, meta, contractID }: { + state: CheloniaState + getters: Getters + meta: JSONType + contractID: string + }) => boolean | void + process: (message: Mutation, { state, getters }: { + state: CheloniaState + getters: Getters + }) => void + sideEffect?: (message: Mutation, { state, getters }: { + state: CheloniaState + getters: Getters + }) => void +} + +export type Mutation = { + data: JSONType + meta: JSONType + hash: string + contractID: string +} + +type PostOp = (state: unknown, message: unknown) => boolean +type PreOp = (state: unknown, message: unknown) => boolean + +type SBP = (selector: string, ...args: unknown[]) => unknown + +type SBPCallParameters = [string, ...unknown[]] + export type ContractDefinition = { - actions: any - getters: any + actions: Record + getters: Getters manifest: string metadata: { - create(): any - validate(meta: any, args: any): void + create(): JSONType + validate(meta: JSONType, args: { + state: CheloniaState + getters: Getters + contractID: string + }): void validate(): void } - methods: any + methods: Record unknown> name: string sbp: SBP - state (contractID: string): any + state (contractID: string): CheloniaState // Contract instance state } -type SBP = (selector: string, ...args: any[]) => any - -export type ChelRegParams = { - contractName: string; - server?: string; // TODO: implement! - data: Object; - hooks?: { - prepublishContract?: (msg: GIMessage) => void; - prepublish?: (msg: GIMessage) => void; - postpublish?: (msg: GIMessage) => void; - }; - publishOptions?: { maxAttempts: number }; -} - -export type ChelActionParams = { - action: string; - server?: string; // TODO: implement! - contractID: string; - data: Object; - hooks?: { - prepublishContract?: (msg: GIMessage) => void; - prepublish?: (msg: GIMessage) => void; - postpublish?: (msg: GIMessage) => void; - }; - publishOptions?: { maxAttempts: number }; +export type ContractInfo = { + file: string + hash: string } export { GIMessage } -export const ACTION_REGEX: RegExp = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ +export const ACTION_REGEX = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ // ACTION_REGEX.exec('gi.contracts/group/payment/process') // 0 => 'gi.contracts/group/payment/process' // 1 => 'gi.contracts/group/payment/' @@ -151,8 +212,8 @@ export default sbp('sbp/selectors/register', { manifests: {} // override! contract names => manifest hashes }, whitelisted: (action: string): boolean => !!this.whitelistedActions[action], - reactiveSet: (obj: any, key: string, value: any): typeof value => { obj[key] = value; return value }, // example: set to Vue.set - reactiveDel: (obj: any, key: string): void => { delete obj[key] }, + reactiveSet: (obj: CheloniaState, key: string, value: unknown): typeof value => { obj[key] = value; return value }, // example: set to Vue.set + reactiveDel: (obj: CheloniaState, key: string): void => { delete obj[key] }, skipActionProcessing: false, skipSideEffects: false, connectionOptions: { @@ -176,8 +237,8 @@ export default sbp('sbp/selectors/register', { } this.manifestToContract = {} this.whitelistedActions = {} - this.sideEffectStacks = {} // [contractID]: Array - this.sideEffectStack = (contractID: string): Array => { + this.sideEffectStacks = {} // [contractID]: Array + this.sideEffectStack = (contractID: string): Array => { let stack = this.sideEffectStacks[contractID] if (!stack) { this.sideEffectStacks[contractID] = stack = [] @@ -245,7 +306,7 @@ export default sbp('sbp/selectors/register', { if (!ACTION_REGEX.exec(contract.name)) throw new Error(`bad contract name: ${contract.name}`) if (!contract.metadata) contract.metadata = { validate () {}, create: () => ({}) } if (!contract.getters) contract.getters = {} - contract.state = (contractID) => sbp(this.config.stateSelector)[contractID] + contract.state = (contractID: string) => sbp(this.config.stateSelector)[contractID] contract.manifest = this.defContractManifest contract.sbp = this.defContractSBP this.defContractSelectors = [] @@ -255,7 +316,7 @@ export default sbp('sbp/selectors/register', { [`${contract.manifest}/${contract.name}/getters`]: () => contract.getters, // 2 ways to cause sideEffects to happen: by defining a sideEffect function in the // contract, or by calling /pushSideEffect w/async SBP call. Can also do both. - [`${contract.manifest}/${contract.name}/pushSideEffect`]: (contractID: string, asyncSbpCall: any[]) => { + [`${contract.manifest}/${contract.name}/pushSideEffect`]: (contractID: string, asyncSbpCall: SBPCallParameters) => { // if this version of the contract is pushing a sideEffect to a function defined by the // contract itself, make sure that it calls the same version of the sideEffect const [sel] = asyncSbpCall @@ -275,7 +336,7 @@ export default sbp('sbp/selectors/register', { // - whatever keys should be passed in as well // base it off of the design of encryptedAction() this.defContractSelectors.push(...sbp('sbp/selectors/register', { - [`${contract.manifest}/${action}/process`]: (message: any, state: any) => { + [`${contract.manifest}/${action}/process`]: (message: Mutation, state: CheloniaState) => { const { meta, data, contractID } = message // TODO: optimize so that you're creating a proxy object only when needed const gProxy = gettersProxy(state, contract.getters) @@ -284,7 +345,7 @@ export default sbp('sbp/selectors/register', { contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) contract.actions[action].process(message, { state, ...gProxy }) }, - [`${contract.manifest}/${action}/sideEffect`]: async (message: any, state: any) => { + [`${contract.manifest}/${action}/sideEffect`]: async (message: Mutation, state: CheloniaState) => { const sideEffects = this.sideEffectStack(message.contractID) while (sideEffects.length > 0) { const sideEffect = sideEffects.shift() @@ -292,15 +353,17 @@ export default sbp('sbp/selectors/register', { const [selector, ...args] = sideEffect await contract.sbp(selector, ...args) } catch (e) { + // @ts-expect-error: TS2339 Property 'description' does not exist on type 'Mutation'. console.error(`[chelonia] ERROR: '${e.name}' ${e.message}, for pushed sideEffect of ${message.description()}:`, sideEffect) this.sideEffectStacks[message.contractID] = [] // clear the side effects throw e } } - if (contract.actions[action].sideEffect) { + const { sideEffect } = contract.actions[action] + if (sideEffect) { state = state || contract.state(message.contractID) const gProxy = gettersProxy(state, contract.getters) - await contract.actions[action].sideEffect(message, { state, ...gProxy }) + await sideEffect(message, { state, ...gProxy }) } } })) @@ -338,7 +401,7 @@ export default sbp('sbp/selectors/register', { } }, // resolves when all pending actions for these contractID(s) finish - 'chelonia/contract/wait': function (contractIDs?: string | string[]): Promise { + 'chelonia/contract/wait': function (contractIDs?: string | string[]): Promise { const listOfIds = contractIDs ? (typeof contractIDs === 'string' ? [contractIDs] : contractIDs) : Object.keys(sbp(this.config.stateSelector).contracts) @@ -348,7 +411,7 @@ export default sbp('sbp/selectors/register', { }, // 'chelonia/contract' - selectors related to injecting remote data and monitoring contracts // TODO: add an optional parameter to "retain" the contract (see #828) - 'chelonia/contract/sync': function (contractIDs: string | string[]): Promise { + 'chelonia/contract/sync': function (contractIDs: string | string[]): Promise { const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs return Promise.all(listOfIds.map(contractID => { // enqueue this invocation in a serial queue to ensure @@ -358,7 +421,7 @@ export default sbp('sbp/selectors/register', { // This prevents handleEvent getting called with the wrong previousHEAD for an event. return sbp('chelonia/queueInvocation', contractID, [ 'chelonia/private/in/syncContract', contractID - ]).catch((err: any) => { + ]).catch((err: unknown) => { console.error(`[chelonia] failed to sync ${contractID}:`, err) throw err // re-throw the error }) @@ -366,7 +429,7 @@ export default sbp('sbp/selectors/register', { }, // TODO: implement 'chelonia/contract/release' (see #828) // safer version of removeImmediately that waits to finish processing events for contractIDs - 'chelonia/contract/remove': function (contractIDs: string | string[]): Promise { + 'chelonia/contract/remove': function (contractIDs: string | string[]): Promise { const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs return Promise.all(listOfIds.map(contractID => { return sbp('chelonia/queueInvocation', contractID, [ @@ -408,7 +471,7 @@ export default sbp('sbp/selectors/register', { return events.reverse().map(b64ToStr) } }, - 'chelonia/out/eventsBetween': async function (startHash: string, endHash: string, offset: number = 0): Promise { + 'chelonia/out/eventsBetween': async function (startHash: string, endHash: string, offset = 0): Promise { if (offset < 0) { console.error('[chelonia] invalid params error: "offset" needs to be positive integer or zero') return @@ -419,7 +482,7 @@ export default sbp('sbp/selectors/register', { return events.reverse().map(b64ToStr) } }, - 'chelonia/latestContractState': async function (contractID: string): Promise { + 'chelonia/latestContractState': async function (contractID: string): Promise { const events = await sbp('chelonia/out/eventsSince', contractID, contractID) let state = {} // fast-path @@ -534,16 +597,18 @@ async function outEncryptedOrUnencryptedAction ( return message } +type Getters = Record unknown> + // The gettersProxy is what makes Vue-like getters possible. In other words, // we want to make sure that the getter functions that we defined in each // contract get passed the 'state' when a getter is accessed. // The only way to pass in the state is by creating a Proxy object that does // that for us. This allows us to maintain compatibility with Vue.js and integrate // the contract getters into the Vue-facing getters. -function gettersProxy (state: any, getters: any): { getters: any } { - const proxyGetters: any = new Proxy({}, { - get (target: any, prop: string) { - return (getters[prop] as any)(state, proxyGetters) +function gettersProxy (state: unknown, getters: Getters): { getters: Getters } { + const proxyGetters: Getters = new Proxy({} as Getters, { + get (target: Getters, prop: string) { + return (getters[prop])(state, proxyGetters) } }) return { getters: proxyGetters } diff --git a/shared/domains/chelonia/db.ts b/shared/domains/chelonia/db.ts index 7cbc46b55c..e237984f0b 100644 --- a/shared/domains/chelonia/db.ts +++ b/shared/domains/chelonia/db.ts @@ -1,11 +1,13 @@ -declare var process: any - import sbp from '@sbp/sbp' import '@sbp/okturtles.data' import '@sbp/okturtles.eventqueue' import { GIMessage } from '~/shared/domains/chelonia/GIMessage.ts' import { ChelErrorDBBadPreviousHEAD, ChelErrorDBConnection } from './errors.ts' +declare const process: { + env: Record +} + const headSuffix = '-HEAD' // NOTE: To enable persistence of log use 'sbp/selectors/overwrite' @@ -17,20 +19,20 @@ const dbPrimitiveSelectors = process.env.LIGHTWEIGHT_CLIENT === 'true' ? { 'chelonia/db/get': function (key: string): Promise { const id = sbp('chelonia/db/contractIdFromLogHEAD', key) - // @ts-ignore Property 'config' does not exist. + // @ts-expect-error Property 'config' does not exist. return Promise.resolve(id ? sbp(this.config.stateSelector).contracts[id]?.HEAD : null) }, - 'chelonia/db/set': function (key: string, value: any): Promise { return Promise.resolve(value) }, + 'chelonia/db/set': function (key: string, value: unknown): Promise { return Promise.resolve(value) }, 'chelonia/db/delete': function (): Promise { return Promise.resolve() } } : { - 'chelonia/db/get': function (key: any): any { + 'chelonia/db/get': function (key: unknown): unknown { return Promise.resolve(sbp('okTurtles.data/get', key)) }, - 'chelonia/db/set': function (key: any, value: any) { + 'chelonia/db/set': function (key: unknown, value: unknown) { return Promise.resolve(sbp('okTurtles.data/set', key, value)) }, - 'chelonia/db/delete': function (key: any) { + 'chelonia/db/delete': function (key: unknown) { return Promise.resolve(sbp('okTurtles.data/delete', key)) } } diff --git a/shared/domains/chelonia/errors.ts b/shared/domains/chelonia/errors.ts index e4a0d25652..5d79ee25fb 100644 --- a/shared/domains/chelonia/errors.ts +++ b/shared/domains/chelonia/errors.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ export class ChelErrorDBBadPreviousHEAD extends Error { // ugly boilerplate because JavaScript is stupid // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types diff --git a/shared/domains/chelonia/internals.ts b/shared/domains/chelonia/internals.ts index 342b5bb8df..53f73fd4db 100644 --- a/shared/domains/chelonia/internals.ts +++ b/shared/domains/chelonia/internals.ts @@ -6,10 +6,9 @@ import { GIOpActionUnencrypted, GIOpContract, GIOpKeyAdd, - GIOpPropSet, - GIOpValue + GIOpPropSet } from './GIMessage.ts' -import type { CheloniaConfig, CheloniaInstance, CheloniaState, ContractDefinition } from './chelonia.ts' +import type { CheloniaConfig, CheloniaInstance, CheloniaState, ContractDefinition, ContractInfo } from './chelonia.ts' import { randomIntFromRange, delay, cloneDeep, debounce, pick } from '~/frontend/model/contracts/shared/giLodash.js' import { ChelErrorUnexpected, ChelErrorUnrecoverable } from './errors.ts' @@ -18,6 +17,37 @@ import { handleFetchResult } from '~/frontend/controller/utils/misc.js' import { blake32Hash } from '~/shared/functions.ts' // import 'ses' +type BoolCallback = (...args: unknown[]) => boolean | void + +type ContractState = { + _vm?: VM +} + +type Key = { + context: string + key: string +} + +type RevertProcessParameters = { + message: GIMessage + state: CheloniaState + contractID: string + contractStateCopy: ContractState +} + +type RevertSideEffectParameters = { + message: GIMessage + state: CheloniaState + contractID: string + contractStateCopy: ContractState + stateCopy: CheloniaState +} + +type VM = { + authorizedKeys: Key[] + props: Record +} + // export const FERAL_FUNCTION = Function export default sbp('sbp/selectors/register', { @@ -33,7 +63,7 @@ export default sbp('sbp/selectors/register', { const manifestURL = `${this.config.connectionURL}/file/${manifestHash}` const manifest = await fetch(manifestURL).then(handleFetchResult('json')) const body = JSON.parse(manifest.body) - const contractInfo = (this.config.contracts.defaults.preferSlim && body.contractSlim) || body.contract + const contractInfo: ContractInfo = (this.config.contracts.defaults.preferSlim && body.contractSlim) || body.contract console.info(`[chelonia] loading contract '${contractInfo.file}'@'${body.version}' from manifest: ${manifestHash}`) const source = await fetch(`${this.config.connectionURL}/file/${contractInfo.hash}`) .then(handleFetchResult('text')) @@ -48,7 +78,7 @@ export default sbp('sbp/selectors/register', { const allowedDoms = this.config.contracts.defaults.allowedDomains .reduce(reduceAllow, {}) let contractName: string // eslint-disable-line prefer-const - const contractSBP = (selector: string, ...args: any[]) => { + const contractSBP = (selector: string, ...args: unknown[]) => { const domain = domainFromSelector(selector) if (selector.startsWith(contractName)) { selector = `${manifestHash}/${selector}` @@ -164,23 +194,24 @@ export default sbp('sbp/selectors/register', { } } }, - 'chelonia/private/in/processMessage': async function (this: CheloniaInstance, message: GIMessage, state: any) { + 'chelonia/private/in/processMessage': async function (this: CheloniaInstance, message: GIMessage, state: ContractState) { const [opT, opV] = message.op() const hash = message.hash() const contractID = message.contractID() const manifestHash = message.manifest() const config = this.config - if (!state._vm) state._vm = {} + if (!state._vm) state._vm = {} as VM + const { _vm } = state const opFns = { [GIMessage.OP_CONTRACT] (v: GIOpContract) { // TODO: shouldn't each contract have its own set of authorized keys? - if (!state._vm.authorizedKeys) state._vm.authorizedKeys = [] + if (!_vm.authorizedKeys) _vm.authorizedKeys = [] // TODO: we probably want to be pushing the de-JSON-ified key here - state._vm.authorizedKeys.push({ key: v.keyJSON, context: 'owner' }) + _vm.authorizedKeys.push({ key: v.keyJSON, context: 'owner' }) }, [GIMessage.OP_ACTION_ENCRYPTED] (v: GIOpActionEncrypted) { if (!config.skipActionProcessing) { - const decrypted = message.decryptedValue(config.decryptFn) + const decrypted = message.decryptedValue(config.decryptFn) as GIOpActionUnencrypted opFns[GIMessage.OP_ACTION_UNENCRYPTED](decrypted) } }, @@ -195,8 +226,8 @@ export default sbp('sbp/selectors/register', { }, [GIMessage.OP_PROP_DEL]: notImplemented, [GIMessage.OP_PROP_SET] (v: GIOpPropSet) { - if (!state._vm.props) state._vm.props = {} - state._vm.props[v.key] = v.value + if (!_vm.props) _vm.props = {} + _vm.props[v.key] = v.value }, [GIMessage.OP_KEY_ADD] (v: GIOpKeyAdd) { // TODO: implement this. consider creating a function so that @@ -215,14 +246,13 @@ export default sbp('sbp/selectors/register', { processOp = config.preOp(message, state) !== false && processOp } if (`preOp_${opT}` in config) { - // @ts-ignore Property 'preOp_ae' does not exist on type 'CheloniaConfig' - processOp = (config[`preOp_${opT}`] as any)(message, state) !== false && processOp + processOp = (config[`preOp_${opT}`] as BoolCallback)(message, state) !== false && processOp } if (processOp && !config.skipProcessing) { + // @ts-expect-error TS2345: Argument of type 'GIOpValue' is not assignable. opFns[opT](opV) config.postOp && config.postOp(message, state) - // @ts-ignore Property 'postOp_ae' does not exist on type 'CheloniaConfig' - ;(`postOp_${opT}` in config) && (config[`postOp_${opT}`] as any)(message, state) + ;(`postOp_${opT}` in config) && (config[`postOp_${opT}`] as BoolCallback)(message, state) } }, 'chelonia/private/in/enqueueHandleEvent': function (this: CheloniaInstance, event: GIMessage) { @@ -372,7 +402,7 @@ const handleEvent = { throw e } }, - async processMutation (this: CheloniaInstance, message: GIMessage, state: any) { + async processMutation (this: CheloniaInstance, message: GIMessage, state: CheloniaState) { const contractID = message.contractID() if (message.isFirstMessage()) { const { type } = message.opValue() as GIOpContract @@ -394,12 +424,12 @@ const handleEvent = { const contractID = message.contractID() const manifestHash = message.manifest() const hash = message.hash() - const { action, data, meta } = message.decryptedValue() + const { action, data, meta } = message.decryptedValue() as GIOpActionUnencrypted const mutation = { data, meta, hash, contractID } await sbp(`${manifestHash}/${action}/sideEffect`, mutation) } }, - revertProcess (this: CheloniaInstance, { message, state, contractID, contractStateCopy }: any) { + revertProcess (this: CheloniaInstance, { message, state, contractID, contractStateCopy }: RevertProcessParameters) { console.warn(`[chelonia] reverting mutation ${message.description()}: ${message.serialize()}. Any side effects will be skipped!`) if (!contractStateCopy) { console.warn(`[chelonia] mutation reversion on very first message for contract ${contractID}! Your contract may be too damaged to be useful and should be redeployed with bugfixes.`) @@ -407,7 +437,7 @@ const handleEvent = { } this.config.reactiveSet(state, contractID, contractStateCopy) }, - revertSideEffect (this: CheloniaInstance, { message, state, contractID, contractStateCopy, stateCopy }: any) { + revertSideEffect (this: CheloniaInstance, { message, state, contractID, contractStateCopy, stateCopy }: RevertSideEffectParameters) { console.warn(`[chelonia] reverting entire state because failed sideEffect for ${message.description()}: ${message.serialize()}`) if (!contractStateCopy) { this.config.reactiveDel(state, contractID) @@ -420,7 +450,7 @@ const handleEvent = { } } -const notImplemented = (v: any) => { +const notImplemented = (v: unknown) => { throw new Error(`chelonia: action not implemented to handle: ${JSON.stringify(v)}.`) } diff --git a/shared/functions.ts b/shared/functions.ts index 5e3ed29387..898cd666e3 100644 --- a/shared/functions.ts +++ b/shared/functions.ts @@ -7,14 +7,14 @@ import blake from 'blakejs' import { Buffer } from 'buffer' if (typeof window === 'object') { - // @ts-ignore + // @ts-expect-error TS2339 [ERROR]: Property 'Buffer' does not exist on type 'Window & typeof globalThis'. window.Buffer = Buffer } else { - // @ts-ignore + // @ts-expect-error TS7017 [ERROR]: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature. globalThis.Buffer = Buffer } -export function blake32Hash (data: any) { +export function blake32Hash (data: unknown) { // TODO: for node/electron, switch to: https://github.com/ludios/node-blake2 const uint8array = blake.blake2b(data, null, 32) // TODO: if we switch to webpack we may need: https://github.com/feross/buffer @@ -31,13 +31,18 @@ export function blake32Hash (data: any) { // These hoops might result in inconsistencies between Node.js and the frontend. export const b64ToBuf = (b64: string) => Buffer.from(b64, 'base64') export const b64ToStr = (b64: string) => b64ToBuf(b64).toString('utf8') -export const bufToB64 = (buf: any) => Buffer.from(buf).toString('base64') +export const bufToB64 = (buf: ArrayBuffer) => Buffer.from(buf).toString('base64') export const strToBuf = (str: string) => Buffer.from(str, 'utf8') export const strToB64 = (str: string) => strToBuf(str).toString('base64') -export const bytesToB64 = (ary: any) => Buffer.from(ary).toString('base64') +export const bytesToB64 = (ary: ArrayBuffer) => Buffer.from(ary).toString('base64') + +type KeyPair = { + publicKey: string + secretKey: string +} export function sign ( - { publicKey, secretKey }: any, + { publicKey, secretKey }: KeyPair, msg = 'hello!', futz = '' ) { diff --git a/shared/pubsub.test.ts b/shared/pubsub.test.ts index 9eecfd0792..2ff67ee081 100644 --- a/shared/pubsub.test.ts +++ b/shared/pubsub.test.ts @@ -1,9 +1,7 @@ import { assert, - assertEquals, - assertMatch, - assertNotMatch -} from "https://deno.land/std@0.153.0/testing/asserts.ts" + assertEquals +} from 'https://deno.land/std@0.153.0/testing/asserts.ts' import '~/scripts/process-shim.ts' @@ -20,7 +18,7 @@ const { minReconnectionDelay } = client.options -const createRandomDelays = (number) => { +const createRandomDelays = (number: number) => { return [...new Array(number)].map((_, i) => { client.failedConnectionAttempts = i return client.getNextRandomDelay() @@ -29,11 +27,11 @@ const createRandomDelays = (number) => { const delays1 = createRandomDelays(10) const delays2 = createRandomDelays(10) - +// Test steps must be async, but we don't always use `await` in them. +/* eslint-disable require-await */ Deno.test({ name: 'Test getNextRandomDelay()', fn: async function (tests) { - await tests.step('every delay should be longer than the previous one', async function () { // In other words, the delays should be sorted in ascending numerical order. assertEquals(delays1, [...delays1].sort((a, b) => a - b)) @@ -59,6 +57,5 @@ Deno.test({ }) }, sanitizeResources: false, - sanitizeOps: false, + sanitizeOps: false }) - diff --git a/shared/pubsub.ts b/shared/pubsub.ts index c2c854f1c2..4d6e2f7c99 100644 --- a/shared/pubsub.ts +++ b/shared/pubsub.ts @@ -1,17 +1,40 @@ -declare var process: any - import sbp from '@sbp/sbp' import '@sbp/okturtles.events' +declare const process: { + env: Record +} + type JSONType = ReturnType; // ====== Types ====== // +type Callback = (this: PubsubClient, ...args: unknown[]) => void + type Message = { [key: string]: JSONType; type: string } +type MessageHandler = (this: PubsubClient, msg: Message) => void + +type PubsubClientOptions = { + handlers?: Record + eventHandlers?: Record + logPingMessages?: boolean + manual?: boolean + maxReconnectionDelay?: number + maxRetries?: number + messageHandlers?: Record + minReconnectionDelay?: number + pingTimeout?: number + reconnectOnDisconnection?: boolean + reconnectOnOnline?: boolean + reconnectOnTimeout?: boolean + reconnectionDelayGrowFactor?: number + timeout?: number +} + // ====== Event name constants ====== // export const PUBSUB_ERROR = 'pubsub-error' @@ -43,33 +66,50 @@ export const RESPONSE_TYPE = Object.freeze({ SUCCESS: 'success' }) +// TODO: verify these are good defaults +const defaultOptions = { + logPingMessages: process.env.NODE_ENV === 'development' && !process.env.CI, + manual: false, + maxReconnectionDelay: 60000, + maxRetries: 10, + pingTimeout: 45000, + minReconnectionDelay: 500, + reconnectOnDisconnection: true, + reconnectOnOnline: true, + // Defaults to false to avoid reconnection attempts in case the server doesn't + // respond because of a failed authentication. + reconnectOnTimeout: false, + reconnectionDelayGrowFactor: 2, + timeout: 5000 +} + export class PubsubClient { - connectionTimeoutID?: number; - customEventHandlers: Record; + connectionTimeoutID?: number + customEventHandlers: Record // The current number of connection attempts that failed. // Reset to 0 upon successful connection. // Used to compute how long to wait before the next reconnection attempt. - failedConnectionAttempts: number; - isLocal: boolean; + failedConnectionAttempts: number + isLocal: boolean // True if this client has never been connected yet. - isNew: boolean; - listeners: Record; - messageHandlers: Record void>; - nextConnectionAttemptDelayID?: number; - options: any; + isNew: boolean + listeners: Record + messageHandlers: Record + nextConnectionAttemptDelayID?: number + options: typeof defaultOptions // Requested subscriptions for which we didn't receive a response yet. - pendingSubscriptionSet: Set; - pendingSyncSet: Set; - pendingUnsubscriptionSet: Set; - pingTimeoutID?: number; - shouldReconnect: boolean; + pendingSubscriptionSet: Set + pendingSyncSet: Set + pendingUnsubscriptionSet: Set + pingTimeoutID?: number + shouldReconnect: boolean // The underlying WebSocket object. // A new one is necessary for every connection or reconnection attempt. - socket: WebSocket | null = null; - subscriptionSet: Set; - url: string; + socket: WebSocket | null = null + subscriptionSet: Set + url: string - constructor (url: string, options: any = {}) { + constructor (url: string, options: PubsubClientOptions = {}) { this.customEventHandlers = options.handlers ?? {} this.failedConnectionAttempts = 0 this.isLocal = /\/\/(localhost|127\.0\.0\.1)([:?/]|$)/.test(url) @@ -95,11 +135,11 @@ export class PubsubClient { // Another benefit is the ability to patch the client protocol at runtime by // updating the client's custom event handler map. for (const name of Object.keys(defaultClientEventHandlers)) { - client.listeners[name] = (event: any) => { + client.listeners[name] = (event: Event) => { try { // Use `.call()` to pass the client via the 'this' binding. - // @ts-ignore - defaultClientEventHandlers[name as SocketEventName]?.call(client, event) + // @ts-expect-error TS2684 + defaultClientEventHandlers[name]?.call(client, event) client.customEventHandlers[name]?.call(client, event) } catch (error) { // Do not throw any error but emit an `error` event instead. @@ -182,7 +222,7 @@ export class PubsubClient { for (const name of socketEventNames) { client.socket.removeEventListener(name, client.listeners[name]) } - client.socket.close(4001, 'destroy') + client.socket.close(4001, 'terminated') } client.listeners = {} client.socket = null @@ -296,7 +336,7 @@ export class PubsubClient { * {number?} timeout=5_000 - Connection timeout duration in milliseconds. * @returns {PubSubClient} */ -export function createClient (url: string, options: any = {}): PubsubClient { +export function createClient (url: string, options: PubsubClientOptions = {}): PubsubClient { return new PubsubClient(url, options) } @@ -568,31 +608,13 @@ const defaultMessageHandlers = { } } -// TODO: verify these are good defaults -const defaultOptions = { - logPingMessages: process.env.NODE_ENV === 'development' && !process.env.CI, - pingTimeout: 45000, - maxReconnectionDelay: 60000, - maxRetries: 10, - minReconnectionDelay: 500, - reconnectOnDisconnection: true, - reconnectOnOnline: true, - // Defaults to false to avoid reconnection attempts in case the server doesn't - // respond because of a failed authentication. - reconnectOnTimeout: false, - reconnectionDelayGrowFactor: 2, - timeout: 5000 -} - const globalEventNames = ['offline', 'online'] const socketEventNames = ['close', 'error', 'message', 'open'] -type SocketEventName = 'close' | 'error' | 'message' | 'open'; - // `navigator.onLine` can give confusing false positives when `true`, // so we'll define `isDefinetelyOffline()` rather than `isOnline()` or `isOffline()`. // See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine -// @ts-ignore TS2339 [ERROR]: Property 'onLine' does not exist on type 'Navigator'. +// @ts-expect-error TS2339 [ERROR]: Property 'onLine' does not exist on type 'Navigator'. const isDefinetelyOffline = () => typeof navigator === 'object' && navigator.onLine === false // Parses and validates a received message. @@ -613,7 +635,7 @@ export const messageParser = (data: string): Message => { // Register custom SBP event listeners before the first connection. for (const name of Object.keys(defaultClientEventHandlers)) { if (name === 'error' || !socketEventNames.includes(name)) { - sbp('okTurtles.events/on', `pubsub-${name}`, (target: PubsubClient, detail: any) => { + sbp('okTurtles.events/on', `pubsub-${name}`, (target: PubsubClient, detail: unknown) => { target.listeners[name](({ type: name, target, detail } as unknown) as Event) }) } diff --git a/test/backend.test.js b/test/backend.test.js deleted file mode 100644 index 0038c4a998..0000000000 --- a/test/backend.test.js +++ /dev/null @@ -1,386 +0,0 @@ -/* eslint-env mocha */ - -import sbp from '@sbp/sbp' -import '@sbp/okturtles.events' -import '@sbp/okturtles.eventqueue' -import '~/shared/domains/chelonia/chelonia.js' -import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' -import { handleFetchResult } from '~/frontend/controller/utils/misc.js' -import { blake32Hash } from '~/shared/functions.js' -import * as Common from '@common/common.js' -import proposals from '~/frontend/model/contracts/shared/voting/proposals.js' -import { PAYMENT_PENDING, PAYMENT_TYPE_MANUAL } from '~/frontend/model/contracts/shared/payments/index.js' -import { INVITE_INITIAL_CREATOR, INVITE_EXPIRES_IN_DAYS, MAIL_TYPE_MESSAGE, PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC } from '~/frontend/model/contracts/shared/constants.js' -import { createInvite } from '~/frontend/model/contracts/shared/functions.js' -import '~/frontend/controller/namespace.js' -import chalk from 'chalk' -import { THEME_LIGHT } from '~/frontend/utils/themes.js' -import manifests from '~/frontend/model/contracts/manifests.json' - -// Necessary since we are going to use a WebSocket pubsub client in the backend. -global.WebSocket = require('ws') -const should = require('should') // eslint-disable-line - -// Remove this when dropping support for Node versions lower than v18. -const Blob = require('buffer').Blob -const fs = require('fs') -const path = require('path') -// const { PassThrough, Readable } = require('stream') - -chalk.level = 2 // for some reason it's not detecting that terminal supports colors -const { bold } = chalk - -// var unsignedMsg = sign(personas[0], 'futz') - -// TODO: replay attacks? (need server-provided challenge for `msg`?) -// nah, this should be taken care of by TLS. However, for message -// passing we should be using a forward-secure protocol. See -// MessageRelay in interface.js. - -// TODO: the request for members of a group should be made with a group -// key or a group signature. There should not be a mapping of a -// member's key to all the groups that they're in (that's unweildy -// and compromises privacy). - -const vuexState = { - currentGroupId: null, - currentChatRoomIDs: {}, - contracts: {}, // contractIDs => { type:string, HEAD:string } (for contracts we've successfully subscribed to) - pending: [], // contractIDs we've just published but haven't received back yet - loggedIn: false, // false | { username: string, identityContractID: string } - theme: THEME_LIGHT, - fontSize: 1, - increasedContrast: false, - namespaceLookups: Object.create(null), - reducedMotion: false, - appLogsFilter: ['error', 'info', 'warn'] -} - -// this is to ensure compatibility between frontend and test/backend.test.js -sbp('okTurtles.data/set', 'API_URL', process.env.API_URL) -sbp('sbp/selectors/register', { - // for handling the loggedIn metadata() in Contracts.js - 'state/vuex/state': () => { - return vuexState - } -}) - -sbp('sbp/selectors/register', { - 'backend.tests/postEntry': async function (entry) { - console.log(bold.yellow('sending entry with hash:'), entry.hash()) - const res = await sbp('chelonia/private/out/publishEvent', entry) - should(res).equal(entry.hash()) - return res - } -}) - -// uncomment this to help with debugging: -// sbp('sbp/filters/global/add', (domain, selector, data) => { -// console.log(`[sbp] ${selector}:`, data) -// }) - -describe('Full walkthrough', function () { - const users = {} - const groups = {} - - it('Should configure chelonia', async function () { - await sbp('chelonia/configure', { - connectionURL: process.env.API_URL, - stateSelector: 'state/vuex/state', - skipSideEffects: true, - connectionOptions: { - reconnectOnDisconnection: false, - reconnectOnOnline: false, - reconnectOnTimeout: false, - timeout: 3000 - }, - contracts: { - ...manifests, - defaults: { - modules: { '@common/common.js': Common }, - allowedSelectors: [ - 'state/vuex/state', 'state/vuex/commit', 'state/vuex/getters', - 'chelonia/contract/sync', 'chelonia/contract/remove', 'controller/router', - 'chelonia/queueInvocation', 'gi.actions/identity/updateLoginStateUponLogin', - 'gi.actions/chatroom/leave', 'gi.notifications/emit' - ], - allowedDomains: ['okTurtles.data', 'okTurtles.events', 'okTurtles.eventQueue'], - preferSlim: true - } - } - }) - }) - - function login (user) { - // we set this so that the metadata on subsequent messages is properly filled in - // currently group and mailbox contracts use this to determine message sender - vuexState.loggedIn = { - username: user.decryptedValue().data.attributes.username, - identityContractID: user.contractID() - } - } - - async function createIdentity (username, email, testFn) { - // append random id to username to prevent conflict across runs - // when GI_PERSIST environment variable is defined - username = `${username}-${Math.floor(Math.random() * 1000)}` - const msg = await sbp('chelonia/out/registerContract', { - contractName: 'gi.contracts/identity', - data: { - // authorizations: [Events.CanModifyAuths.dummyAuth(name)], - attributes: { username, email } - }, - hooks: { - prepublish: (message) => { message.decryptedValue(JSON.parse) }, - postpublish: (message) => { testFn && testFn(message) } - } - }) - return msg - } - function createGroup (name: string, hooks: Object = {}): Promise { - const initialInvite = createInvite({ - quantity: 60, - creator: INVITE_INITIAL_CREATOR, - expires: INVITE_EXPIRES_IN_DAYS.ON_BOARDING - }) - return sbp('chelonia/out/registerContract', { - contractName: 'gi.contracts/group', - data: { - invites: { - [initialInvite.inviteSecret]: initialInvite - }, - settings: { - // authorizations: [Events.CanModifyAuths.dummyAuth(name)], - groupName: name, - groupPicture: '', - sharedValues: 'our values', - mincomeAmount: 1000, - mincomeCurrency: 'USD', - distributionDate: new Date().toISOString(), - minimizeDistribution: true, - proposals: { - [PROPOSAL_GROUP_SETTING_CHANGE]: proposals[PROPOSAL_GROUP_SETTING_CHANGE].defaults, - [PROPOSAL_INVITE_MEMBER]: proposals[PROPOSAL_INVITE_MEMBER].defaults, - [PROPOSAL_REMOVE_MEMBER]: proposals[PROPOSAL_REMOVE_MEMBER].defaults, - [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposals[PROPOSAL_PROPOSAL_SETTING_CHANGE].defaults, - [PROPOSAL_GENERIC]: proposals[PROPOSAL_GENERIC].defaults - } - } - }, - hooks - }) - } - function createPaymentTo (to, amount, contractID, currency = 'USD'): Promise { - return sbp('chelonia/out/actionEncrypted', { - action: 'gi.contracts/group/payment', - data: { - toUser: to.decryptedValue().data.attributes.username, - amount: amount, - currency: currency, - txid: String(parseInt(Math.random() * 10000000)), - status: PAYMENT_PENDING, - paymentType: PAYMENT_TYPE_MANUAL - }, - contractID - }) - } - - async function createMailboxFor (user) { - const mailbox = await sbp('chelonia/out/registerContract', { - contractName: 'gi.contracts/mailbox', - data: {} - }) - await sbp('chelonia/out/actionEncrypted', { - action: 'gi.contracts/identity/setAttributes', - data: { mailbox: mailbox.contractID() }, - contractID: user.contractID() - }) - user.mailbox = mailbox - await sbp('chelonia/contract/sync', mailbox.contractID()) - return mailbox - } - - describe('Identity tests', function () { - it('Should create identity contracts for Alice and Bob', async function () { - users.bob = await createIdentity('bob', 'bob@okturtles.com') - users.alice = await createIdentity('alice', 'alice@okturtles.org') - // verify attribute creation and state initialization - users.bob.decryptedValue().data.attributes.username.should.match(/^bob/) - users.bob.decryptedValue().data.attributes.email.should.equal('bob@okturtles.com') - }) - - it('Should register Alice and Bob in the namespace', async function () { - const { alice, bob } = users - let res = await sbp('namespace/register', alice.decryptedValue().data.attributes.username, alice.contractID()) - // NOTE: don't rely on the return values for 'namespace/register' - // too much... in the future we might remove these checks - res.value.should.equal(alice.contractID()) - res = await sbp('namespace/register', bob.decryptedValue().data.attributes.username, bob.contractID()) - res.value.should.equal(bob.contractID()) - alice.socket = 'hello' - should(alice.socket).equal('hello') - }) - - it('Should verify namespace lookups work', async function () { - const { alice } = users - const res = await sbp('namespace/lookup', alice.decryptedValue().data.attributes.username) - res.should.equal(alice.contractID()) - const contractID = await sbp('namespace/lookup', 'susan') - should(contractID).equal(null) - }) - - it('Should open socket for Alice', async function () { - users.alice.socket = await sbp('chelonia/connect') - }) - - it('Should create mailboxes for Alice and Bob and subscribe', async function () { - // Object.values(users).forEach(async user => await createMailboxFor(user)) - await createMailboxFor(users.alice) - await createMailboxFor(users.bob) - }) - }) - - describe('Group tests', function () { - it('Should create a group & subscribe Alice', async function () { - // set user Alice as being logged in so that metadata on messages is properly set - login(users.alice) - groups.group1 = await createGroup('group1') - await sbp('chelonia/contract/sync', groups.group1.contractID()) - }) - - // NOTE: The frontend needs to use the `fetch` API instead of superagent because - // superagent doesn't support streaming, whereas fetch does. - // TODO: We should also remove superagent as a dependency since `fetch` does - // everything we need. Use fetch from now on. - it('Should get mailbox info for Bob', async function () { - // 1. look up bob's username to get his identity contract - const { bob } = users - const bobsName = bob.decryptedValue().data.attributes.username - const bobsContractId = await sbp('namespace/lookup', bobsName) - should(bobsContractId).equal(bob.contractID()) - // 2. fetch all events for his identity contract to get latest state for it - const state = await sbp('chelonia/latestContractState', bobsContractId) - console.log(bold.red('FINAL STATE:'), state) - // 3. get bob's mailbox contractID from his identity contract attributes - should(state.attributes.mailbox).equal(bob.mailbox.contractID()) - // 4. fetch the latest hash for bob's mailbox. - // we don't need latest state for it just latest hash - const res = await sbp('chelonia/out/latestHash', state.attributes.mailbox) - should(res).equal(bob.mailbox.hash()) - }) - - it("Should invite Bob to Alice's group", function (done) { - const mailbox = users.bob.mailbox - sbp('chelonia/out/actionEncrypted', { - action: 'gi.contracts/mailbox/postMessage', - data: { - from: users.bob.decryptedValue().data.attributes.username, - messageType: MAIL_TYPE_MESSAGE, - message: groups.group1.contractID() - }, - contractID: mailbox.contractID(), - hooks: { - prepublish (invite: GIMessage) { - sbp('okTurtles.events/once', invite.hash(), (contractID: string, entry: GIMessage) => { - console.debug('Bob successfully got invite!') - should(entry.decryptedValue().data.message).equal(groups.group1.contractID()) - done() - }) - } - } - }) - }) - - it('Should post an event', function () { - return createPaymentTo(users.bob, 100, groups.group1.contractID()) - }) - - it('Should sync group and verify payments in state', async function () { - await sbp('chelonia/contract/sync', groups.group1.contractID()) - should(Object.keys(vuexState[groups.group1.contractID()].payments).length).equal(1) - }) - - it('Should fail with wrong contractID', async function () { - try { - await createPaymentTo(users.bob, 100, '') - return Promise.reject(new Error("shouldn't get here!")) - } catch (e) { - return Promise.resolve() - } - }) - - // TODO: these events, as well as all messages sent over the sockets - // should all be authenticated and identified by the user's - // identity contract - }) - - describe('File upload', function () { - it('Should upload "avatar-default.png" as "multipart/form-data"', async function () { - const form = new FormData() - const filepath = './frontend/assets/images/user-avatar-default.png' - // const context = blake2bInit(32, null) - // const stream = fs.createReadStream(filepath) - // // the problem here is that we need to get the hash of the file - // // but doing so consumes the stream, invalidating it and making it - // // so that we can't simply do `form.append('data', stream)` - // // I tried creating a secondary piped stream and sending that instead, - // // however that didn't work. - // // const pass = new PassThrough() // couldn't get this or Readable to work no matter how I tried - // // So instead we save the raw buffer and send that, using a hack - // // to work around a weird bug in hapi or form-data where we have to - // // specify the filename or otherwise the backend treats the data as a string, - // // resulting in the wrong hash for some reason. By specifying `filename` the backend - // // treats it as a Buffer, and we get the correct file hash. - // // We could of course simply read the file twice, but that seems even more hackish. - // var buffer - // const hash = await new Promise((resolve, reject) => { - // stream.on('error', e => reject(e)) - // stream.on('data', chunk => { - // buffer = buffer ? Buffer.concat([buffer, chunk]) : chunk - // blake2bUpdate(context, chunk) - // }) - // stream.on('end', () => { - // const uint8array = blake2bFinal(context) - // resolve(multihash.toB58String(multihash.encode(Buffer.from(uint8array.buffer), 'blake2b-32', 32))) - // }) - // }) - // since we're just saving the buffer now, we might as well use the simpler readFileSync API - const buffer = fs.readFileSync(filepath) - const hash = blake32Hash(buffer) - console.log(`hash for ${path.basename(filepath)}: ${hash}`) - form.append('hash', hash) - form.append('data', new Blob([buffer]), path.basename(filepath)) - await fetch(`${process.env.API_URL}/file`, { method: 'POST', body: form }) - .then(handleFetchResult('text')) - .then(r => should(r).equal(`/file/${hash}`)) - }) - }) - - describe('Cleanup', function () { - it('Should destroy all opened sockets', function () { - // The code below was originally Object.values(...) but changed to .keys() - // due to a similar flow issue to https://github.com/facebook/flow/issues/2221 - Object.keys(users).forEach((userKey) => { - users[userKey].socket && users[userKey].socket.destroy() - }) - }) - }) -}) - -// Potentially useful for dealing with fetch API: -// function streamToPromise (stream, dataHandler) { -// return new Promise((resolve, reject) => { -// stream.on('data', (...args) => { -// try { dataHandler(...args) } catch (err) { reject(err) } -// }) -// stream.on('end', resolve) -// stream.on('error', reject) -// }) -// } -// see: https://github.com/bitinn/node-fetch/blob/master/test/test.js -// This used to be in the 'Should get mailbox info for Bob' test, before the -// server manually created a JSON array out of the objects being streamed. -// await streamToPromise(res.body, chunk => { -// console.log(bold.red('CHUNK:'), chunk.toString()) -// events.push(JSON.parse(chunk.toString())) -// }) diff --git a/test/backend.test.ts b/test/backend.test.ts index 4a3b52e94e..1be2983dad 100644 --- a/test/backend.test.ts +++ b/test/backend.test.ts @@ -115,9 +115,10 @@ Deno.test({ // Wait for the server to be ready. let t0 = Date.now() - let timeout = 3000 + let timeout = 30000 await new Promise((resolve, reject) => { (function ping () { + console.log(process.env.API_URL) fetch(process.env.API_URL).then(resolve).catch(() => { if (Date.now() > t0 + timeout) { reject(new Error('Test setup timed out.')) diff --git a/test/contracts/chatroom.js b/test/contracts/chatroom.js index c2fbe19935..c194739571 100644 --- a/test/contracts/chatroom.js +++ b/test/contracts/chatroom.js @@ -409,14 +409,14 @@ var objectOf = (typeObj, _scope = "Object") => { } const undefAttr = typeAttrs.find((property) => { const propertyTypeFn = typeObj[property]; - return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); }); if (undefAttr) { throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); } const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { const typeFn = typeObj[key]; - if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { return Object.assign(acc, {}); } else { return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); @@ -425,7 +425,10 @@ var objectOf = (typeObj, _scope = "Object") => { return typeAttrs.reduce(reducer, {}); } object2.type = () => { - const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); return `{| ${props.join(",\n ")} |}`; diff --git a/test/contracts/chatroom.js.map b/test/contracts/chatroom.js.map index 2aba20bd13..de86a0719b 100644 --- a/test/contracts/chatroom.js.map +++ b/test/contracts/chatroom.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/shared/time.js", "../../frontend/views/utils/misc.js", "../../frontend/model/contracts/shared/functions.js", "../../frontend/model/contracts/shared/nativeNotification.js", "../../frontend/model/contracts/chatroom.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nexport function logExceptNavigationDuplicated (err ) {\n err.name !== 'NavigationDuplicated' && console.error(err)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { INVITE_STATUS, MESSAGE_TYPES } from './constants.js'\nimport { DAYS_MILLIS } from './time.js'\nimport { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js'\n\n// !!!!!!!!!!!!!!!\n// !! IMPORTANT !!\n// !!!!!!!!!!!!!!!\n//\n// DO NOT CHANGE THE LOGIC TO ANY OF THESE FUNCTIONS!\n// INSTEAD, CREATE NEW FUNCTIONS WITH DIFFERENT NAMES\n// AND USE THOSE INSTEAD!\n//\n// THIS IS A CONSEQUENCE OF SHARING THIS CODE WITH THE REST OF THE APP.\n// IF YOU DO NOT NEED TO SHARE CODE WITH THE REST OF THE APP (AND CAN\n// KEEP IT WITHIN THE CONTRACT ONLY), THEN YOU DON'T NEED TO WORRY ABOUT\n// THIS, AND SHOULD INCLUDE THOSE FUNCTIONS (WITHOUT EXPORTING THEM),\n// DIRECTLY IN YOUR CONTRACT DEFINITION FILE. THEN YOU CAN MODIFY\n// THEM AS MUCH AS YOU LIKE (and generate new contract versions out of them).\n\n// group.js related\n\nexport function createInvite ({ quantity = 1, creator, expires, invitee } \n \n ) \n \n \n \n \n \n \n \n {\n return {\n inviteSecret: `${parseInt(Math.random() * 10000)}`, // TODO: this\n quantity,\n creator,\n invitee,\n status: INVITE_STATUS.VALID,\n responses: {}, // { bob: true } list of usernames that accepted the invite.\n expires: Date.now() + DAYS_MILLIS * expires\n }\n}\n\n// chatroom.js related\n\nexport function createMessage ({ meta, data, hash, state } \n \n ) {\n const { type, text, replyingMessage } = data\n const { createdDate } = meta\n\n let newMessage = {\n type,\n datetime: new Date(createdDate).toISOString(),\n id: hash,\n from: meta.username\n }\n\n if (type === MESSAGE_TYPES.TEXT) {\n newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }\n } else if (type === MESSAGE_TYPES.POLL) {\n // TODO: Poll message creation\n } else if (type === MESSAGE_TYPES.NOTIFICATION) {\n const params = {\n channelName: state?.attributes.name,\n channelDescription: state?.attributes.description,\n ...data.notification\n }\n delete params.type\n newMessage = {\n ...newMessage,\n notification: { type: data.notification.type, params }\n }\n } else if (type === MESSAGE_TYPES.INTERACTIVE) {\n // TODO: Interactive message creation for proposals\n }\n return newMessage\n}\n\nexport async function leaveChatRoom ({ contractID } \n \n ) {\n const rootState = sbp('state/vuex/state')\n const rootGetters = sbp('state/vuex/getters')\n if (contractID === rootGetters.currentChatRoomId) {\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {\n groupId: rootState.currentGroupId\n })\n const curRouteName = sbp('controller/router').history.current.name\n if (curRouteName === 'GroupChat' || curRouteName === 'GroupChatConversation') {\n await sbp('controller/router')\n .push({ name: 'GroupChatConversation', params: { chatRoomId: rootGetters.currentChatRoomId } })\n .catch(logExceptNavigationDuplicated)\n }\n }\n\n sbp('state/vuex/commit', 'deleteChatRoomUnread', { chatRoomId: contractID })\n sbp('state/vuex/commit', 'deleteChatRoomScrollPosition', { chatRoomId: contractID })\n\n // NOTE: make sure *not* to await on this, since that can cause\n // a potential deadlock. See same warning in sideEffect for\n // 'gi.contracts/group/removeMember'\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e)\n })\n}\n\nexport function findMessageIdx (id , messages ) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].id === id) {\n return i\n }\n }\n return -1\n}\n\nexport function makeMentionFromUsername (username ) \n \n {\n return {\n me: `@${username}`,\n all: '@all'\n }\n}\n", "'use strict'\nimport sbp from '@sbp/sbp'\n\n// NOTE: since these functions don't modify contract state, it should\n// be safe to modify them without worrying about version conflicts.\n\nexport async function requestNotificationPermission (force = false) {\n if (typeof Notification === 'undefined') {\n return null\n }\n if (force || Notification.permission === 'default') {\n try {\n sbp('state/vuex/commit', 'setNotificationEnabled', await Notification.requestPermission() === 'granted')\n } catch (e) {\n console.error('requestNotificationPermission:', e.message)\n return null\n }\n }\n return Notification.permission\n}\n\nexport function makeNotification ({ title, body, icon, path } \n \n ) {\n const notificationEnabled = sbp('state/vuex/state').notificationEnabled\n if (typeof Notification === 'undefined' || Notification.permission !== 'granted' || !notificationEnabled) {\n return\n }\n\n const notification = new Notification(title, { body, icon })\n if (path) {\n notification.onclick = function (event) {\n event.preventDefault()\n sbp('controller/router').push({ path }).catch(console.warn)\n }\n }\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, L } from '@common/common.js'\nimport { merge, cloneDeep } from './shared/giLodash.js'\nimport {\n CHATROOM_NAME_LIMITS_IN_CHARS,\n CHATROOM_DESCRIPTION_LIMITS_IN_CHARS,\n CHATROOM_ACTIONS_PER_PAGE,\n CHATROOM_MESSAGES_PER_PAGE,\n MESSAGE_TYPES,\n MESSAGE_NOTIFICATIONS,\n CHATROOM_MESSAGE_ACTION,\n MESSAGE_RECEIVE\n} from './shared/constants.js'\nimport { chatRoomAttributesType, messageType } from './shared/types.js'\nimport { createMessage, leaveChatRoom, findMessageIdx, makeMentionFromUsername } from './shared/functions.js'\nimport { makeNotification } from './shared/nativeNotification.js'\nimport { objectOf, string, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction createNotificationData (\n notificationType ,\n moreParams = {}\n) {\n return {\n type: MESSAGE_TYPES.NOTIFICATION,\n notification: {\n type: notificationType,\n ...moreParams\n }\n }\n}\n\nfunction emitMessageEvent ({ contractID, hash } \n \n \n ) {\n sbp('okTurtles.events/emit', `${CHATROOM_MESSAGE_ACTION}-${contractID}`, { hash })\n}\n\nfunction addMention ({ contractID, messageId, datetime, text, username, chatRoomName } \n \n \n \n \n \n \n ) {\n /**\n * If 'READY_TO_JOIN_CHATROOM' is false, it means not syncing chatroom\n */\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n return\n }\n\n sbp('state/vuex/commit', 'addChatRoomUnreadMention', {\n chatRoomId: contractID,\n messageId,\n createdDate: datetime\n })\n\n const rootGetters = sbp('state/vuex/getters')\n const groupID = rootGetters.groupIdFromChatRoomId(contractID)\n const path = `/group-chat/${contractID}`\n\n makeNotification({\n title: `# ${chatRoomName}`,\n body: text,\n icon: rootGetters.globalProfile2(groupID, username).picture,\n path\n })\n\n sbp('okTurtles.events/emit', MESSAGE_RECEIVE)\n}\n\nfunction deleteMention ({ contractID, messageId } \n \n ) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadMention', { chatRoomId: contractID, messageId })\n}\n\nfunction updateUnreadPosition ({ contractID, hash, createdDate } \n \n ) {\n sbp('state/vuex/commit', 'setChatRoomUnreadSince', {\n chatRoomId: contractID,\n messageId: hash,\n createdDate\n })\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/chatroom',\n metadata: {\n validate: objectOf({\n createdDate: string, // action created date\n username: string, // action creator\n identityContractID: string // action creator identityContractID\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n getters: {\n currentChatRoomState (state) {\n return state\n },\n chatRoomSettings (state, getters) {\n return getters.currentChatRoomState.settings || {}\n },\n chatRoomAttributes (state, getters) {\n return getters.currentChatRoomState.attributes || {}\n },\n chatRoomUsers (state, getters) {\n return getters.currentChatRoomState.users || {}\n },\n chatRoomLatestMessages (state, getters) {\n return getters.currentChatRoomState.messages || []\n }\n },\n actions: {\n // This is the constructor of Chat contract\n 'gi.contracts/chatroom': {\n validate: objectOf({\n attributes: chatRoomAttributesType\n }),\n process ({ meta, data }, { state }) {\n const initialState = merge({\n settings: {\n actionsPerPage: CHATROOM_ACTIONS_PER_PAGE,\n messagesPerPage: CHATROOM_MESSAGES_PER_PAGE,\n maxNameLength: CHATROOM_NAME_LIMITS_IN_CHARS,\n maxDescriptionLength: CHATROOM_DESCRIPTION_LIMITS_IN_CHARS\n },\n attributes: {\n creator: meta.username,\n deletedDate: null,\n archivedDate: null\n },\n users: {},\n messages: []\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/chatroom/join': {\n validate: objectOf({\n username: string // username of joining member\n }),\n process ({ data, meta, hash }, { state }) {\n const { username } = data\n if (!state.saveMessage && state.users[username]) {\n // this can happen when we're logging in on another machine, and also in other circumstances\n console.warn('Can not join the chatroom which you are already part of')\n return\n }\n\n Vue.set(state.users, username, { joinedDate: meta.createdDate })\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = username === meta.username ? MESSAGE_NOTIFICATIONS.JOIN_MEMBER : MESSAGE_NOTIFICATIONS.ADD_MEMBER\n const notificationData = createNotificationData(\n notificationType,\n notificationType === MESSAGE_NOTIFICATIONS.ADD_MEMBER ? { username } : {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM') || // Join by himself or Login in another device\n sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID') === contractID) { // Be added by another\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/rename': {\n validate: objectOf({\n name: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'name', data.name)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_NAME, {})\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/changeDescription': {\n validate: objectOf({\n description: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'description', data.description)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(\n MESSAGE_NOTIFICATIONS.UPDATE_DESCRIPTION, {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/leave': {\n validate: objectOf({\n username: optional(string), // coming from the gi.contracts/group/leaveChatRoom\n member: string // username to be removed\n }),\n process ({ data, meta, hash }, { state }) {\n const { member } = data\n const isKicked = data.username && member !== data.username\n if (!state.saveMessage && !state.users[member]) {\n throw new Error(`Can not leave the chatroom which ${member} are not part of`)\n }\n Vue.delete(state.users, member)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = !isKicked ? MESSAGE_NOTIFICATIONS.LEAVE_MEMBER : MESSAGE_NOTIFICATIONS.KICK_MEMBER\n const notificationData = createNotificationData(notificationType, isKicked ? { username: member } : {})\n const newMessage = createMessage({\n meta: isKicked ? meta : { ...meta, username: member },\n hash,\n data: notificationData,\n state\n })\n state.messages.push(newMessage)\n },\n sideEffect ({ data, hash, contractID, meta }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (data.member === rootState.loggedIn.username) {\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/delete': {\n validate: (data, { state, meta }) => {\n if (state.attributes.creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state, rootState }) {\n Vue.set(state.attributes, 'deletedDate', meta.createdDate)\n for (const username in state.users) {\n Vue.delete(state.users, username)\n }\n },\n sideEffect ({ meta, contractID }, { state }) {\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n },\n 'gi.contracts/chatroom/addMessage': {\n validate: messageType,\n process ({ data, meta, hash }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const pendingMsg = state.messages.find(msg => msg.id === hash && msg.pending)\n if (pendingMsg) {\n delete pendingMsg.pending\n } else {\n state.messages.push(createMessage({ meta, data, hash, state }))\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { state, getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const newMessage = createMessage({ meta, data, hash, state })\n const mentions = makeMentionFromUsername(me)\n if (data.type === MESSAGE_TYPES.TEXT &&\n (newMessage.text.includes(mentions.me) || newMessage.text.includes(mentions.all))) {\n addMention({\n contractID,\n messageId: newMessage.id,\n datetime: newMessage.datetime,\n text: newMessage.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n }\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/editMessage': {\n validate: (data, { state, meta }) => {\n objectOf({\n id: string,\n createdDate: string,\n text: string\n })(data)\n // TODO: Actually NOT SURE it's needed to check if the meta.username === message.from\n // there is no messagess in vuex state\n // to check if the meta.username is creator seems like too heavy\n },\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0 && meta.username === state.messages[msgIndex].from) {\n state.messages[msgIndex].text = data.text\n state.messages[msgIndex].updatedDate = meta.createdDate\n if (state.saveMessage && state.messages[msgIndex].pending) {\n delete state.messages[msgIndex].pending\n }\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const isAlreadyAdded = rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)\n const mentions = makeMentionFromUsername(me)\n const isIncludeMention = data.text.includes(mentions.me) || data.text.includes(mentions.all)\n if (!isAlreadyAdded && isIncludeMention) {\n addMention({\n contractID,\n messageId: data.id,\n /*\n * the following datetime is the time when the message(which made mention) is created\n * the reason why it is it instead of datetime when the mention created is because\n * it is compared to the datetime of other messages when user scrolls\n * to decide if it should be removed from the list of mentions or not\n */\n datetime: data.createdDate,\n text: data.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n } else if (isAlreadyAdded && !isIncludeMention) {\n deleteMention({ contractID, messageId: data.id })\n }\n }\n },\n 'gi.contracts/chatroom/deleteMessage': {\n validate: objectOf({\n id: string\n }),\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0) {\n state.messages.splice(msgIndex, 1)\n }\n // filter replied messages and check if the current message is original\n for (const message of state.messages) {\n if (message.replyingMessage?.id === data.id) {\n message.replyingMessage.id = null\n message.replyingMessage.text = 'Original message was removed.'\n }\n }\n },\n sideEffect ({ data, contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (rootState.chatRoomScrollPosition[contractID] === data.id) {\n sbp('state/vuex/commit', 'setChatRoomScrollPosition', {\n chatRoomId: contractID, messageId: null\n })\n }\n\n if (rootState.chatRoomUnread[contractID].since.messageId === data.id) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadSince', {\n chatRoomId: contractID,\n deletedDate: meta.createdDate\n })\n }\n\n if (me === meta.username) {\n return\n }\n if (rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)) {\n deleteMention({ contractID, messageId: data.id })\n }\n\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/makeEmotion': {\n validate: objectOf({\n id: string,\n emoticon: string\n }),\n process ({ data, meta, contractID }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const { id, emoticon } = data\n const msgIndex = findMessageIdx(id, state.messages)\n if (msgIndex >= 0) {\n let emoticons = cloneDeep(state.messages[msgIndex].emoticons || {})\n if (emoticons[emoticon]) {\n const alreadyAdded = emoticons[emoticon].indexOf(meta.username)\n if (alreadyAdded >= 0) {\n emoticons[emoticon].splice(alreadyAdded, 1)\n if (!emoticons[emoticon].length) {\n delete emoticons[emoticon]\n if (!Object.keys(emoticons).length) {\n emoticons = null\n }\n }\n } else {\n emoticons[emoticon].push(meta.username)\n }\n } else {\n emoticons[emoticon] = [meta.username]\n }\n if (emoticons) {\n Vue.set(state.messages[msgIndex], 'emoticons', emoticons)\n } else {\n Vue.delete(state.messages[msgIndex], 'emoticons')\n }\n }\n },\n sideEffect ({ contractID, hash }) {\n emitMessageEvent({ contractID, hash })\n }\n }\n }\n})\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nexport function logExceptNavigationDuplicated (err ) {\n err.name !== 'NavigationDuplicated' && console.error(err)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { INVITE_STATUS, MESSAGE_TYPES } from './constants.js'\nimport { DAYS_MILLIS } from './time.js'\nimport { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js'\n\n// !!!!!!!!!!!!!!!\n// !! IMPORTANT !!\n// !!!!!!!!!!!!!!!\n//\n// DO NOT CHANGE THE LOGIC TO ANY OF THESE FUNCTIONS!\n// INSTEAD, CREATE NEW FUNCTIONS WITH DIFFERENT NAMES\n// AND USE THOSE INSTEAD!\n//\n// THIS IS A CONSEQUENCE OF SHARING THIS CODE WITH THE REST OF THE APP.\n// IF YOU DO NOT NEED TO SHARE CODE WITH THE REST OF THE APP (AND CAN\n// KEEP IT WITHIN THE CONTRACT ONLY), THEN YOU DON'T NEED TO WORRY ABOUT\n// THIS, AND SHOULD INCLUDE THOSE FUNCTIONS (WITHOUT EXPORTING THEM),\n// DIRECTLY IN YOUR CONTRACT DEFINITION FILE. THEN YOU CAN MODIFY\n// THEM AS MUCH AS YOU LIKE (and generate new contract versions out of them).\n\n// group.js related\n\nexport function createInvite ({ quantity = 1, creator, expires, invitee } \n \n ) \n \n \n \n \n \n \n \n {\n return {\n inviteSecret: `${parseInt(Math.random() * 10000)}`, // TODO: this\n quantity,\n creator,\n invitee,\n status: INVITE_STATUS.VALID,\n responses: {}, // { bob: true } list of usernames that accepted the invite.\n expires: Date.now() + DAYS_MILLIS * expires\n }\n}\n\n// chatroom.js related\n\nexport function createMessage ({ meta, data, hash, state } \n \n ) {\n const { type, text, replyingMessage } = data\n const { createdDate } = meta\n\n let newMessage = {\n type,\n datetime: new Date(createdDate).toISOString(),\n id: hash,\n from: meta.username\n }\n\n if (type === MESSAGE_TYPES.TEXT) {\n newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }\n } else if (type === MESSAGE_TYPES.POLL) {\n // TODO: Poll message creation\n } else if (type === MESSAGE_TYPES.NOTIFICATION) {\n const params = {\n channelName: state?.attributes.name,\n channelDescription: state?.attributes.description,\n ...data.notification\n }\n delete params.type\n newMessage = {\n ...newMessage,\n notification: { type: data.notification.type, params }\n }\n } else if (type === MESSAGE_TYPES.INTERACTIVE) {\n // TODO: Interactive message creation for proposals\n }\n return newMessage\n}\n\nexport async function leaveChatRoom ({ contractID } \n \n ) {\n const rootState = sbp('state/vuex/state')\n const rootGetters = sbp('state/vuex/getters')\n if (contractID === rootGetters.currentChatRoomId) {\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {\n groupId: rootState.currentGroupId\n })\n const curRouteName = sbp('controller/router').history.current.name\n if (curRouteName === 'GroupChat' || curRouteName === 'GroupChatConversation') {\n await sbp('controller/router')\n .push({ name: 'GroupChatConversation', params: { chatRoomId: rootGetters.currentChatRoomId } })\n .catch(logExceptNavigationDuplicated)\n }\n }\n\n sbp('state/vuex/commit', 'deleteChatRoomUnread', { chatRoomId: contractID })\n sbp('state/vuex/commit', 'deleteChatRoomScrollPosition', { chatRoomId: contractID })\n\n // NOTE: make sure *not* to await on this, since that can cause\n // a potential deadlock. See same warning in sideEffect for\n // 'gi.contracts/group/removeMember'\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e)\n })\n}\n\nexport function findMessageIdx (id , messages ) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].id === id) {\n return i\n }\n }\n return -1\n}\n\nexport function makeMentionFromUsername (username ) \n \n {\n return {\n me: `@${username}`,\n all: '@all'\n }\n}\n", "'use strict'\nimport sbp from '@sbp/sbp'\n\n// NOTE: since these functions don't modify contract state, it should\n// be safe to modify them without worrying about version conflicts.\n\nexport async function requestNotificationPermission (force = false) {\n if (typeof Notification === 'undefined') {\n return null\n }\n if (force || Notification.permission === 'default') {\n try {\n sbp('state/vuex/commit', 'setNotificationEnabled', await Notification.requestPermission() === 'granted')\n } catch (e) {\n console.error('requestNotificationPermission:', e.message)\n return null\n }\n }\n return Notification.permission\n}\n\nexport function makeNotification ({ title, body, icon, path } \n \n ) {\n const notificationEnabled = sbp('state/vuex/state').notificationEnabled\n if (typeof Notification === 'undefined' || Notification.permission !== 'granted' || !notificationEnabled) {\n return\n }\n\n const notification = new Notification(title, { body, icon })\n if (path) {\n notification.onclick = function (event) {\n event.preventDefault()\n sbp('controller/router').push({ path }).catch(console.warn)\n }\n }\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, L } from '@common/common.js'\nimport { merge, cloneDeep } from './shared/giLodash.js'\nimport {\n CHATROOM_NAME_LIMITS_IN_CHARS,\n CHATROOM_DESCRIPTION_LIMITS_IN_CHARS,\n CHATROOM_ACTIONS_PER_PAGE,\n CHATROOM_MESSAGES_PER_PAGE,\n MESSAGE_TYPES,\n MESSAGE_NOTIFICATIONS,\n CHATROOM_MESSAGE_ACTION,\n MESSAGE_RECEIVE\n} from './shared/constants.js'\nimport { chatRoomAttributesType, messageType } from './shared/types.js'\nimport { createMessage, leaveChatRoom, findMessageIdx, makeMentionFromUsername } from './shared/functions.js'\nimport { makeNotification } from './shared/nativeNotification.js'\nimport { objectOf, string, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction createNotificationData (\n notificationType ,\n moreParams = {}\n) {\n return {\n type: MESSAGE_TYPES.NOTIFICATION,\n notification: {\n type: notificationType,\n ...moreParams\n }\n }\n}\n\nfunction emitMessageEvent ({ contractID, hash } \n \n \n ) {\n sbp('okTurtles.events/emit', `${CHATROOM_MESSAGE_ACTION}-${contractID}`, { hash })\n}\n\nfunction addMention ({ contractID, messageId, datetime, text, username, chatRoomName } \n \n \n \n \n \n \n ) {\n /**\n * If 'READY_TO_JOIN_CHATROOM' is false, it means not syncing chatroom\n */\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n return\n }\n\n sbp('state/vuex/commit', 'addChatRoomUnreadMention', {\n chatRoomId: contractID,\n messageId,\n createdDate: datetime\n })\n\n const rootGetters = sbp('state/vuex/getters')\n const groupID = rootGetters.groupIdFromChatRoomId(contractID)\n const path = `/group-chat/${contractID}`\n\n makeNotification({\n title: `# ${chatRoomName}`,\n body: text,\n icon: rootGetters.globalProfile2(groupID, username).picture,\n path\n })\n\n sbp('okTurtles.events/emit', MESSAGE_RECEIVE)\n}\n\nfunction deleteMention ({ contractID, messageId } \n \n ) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadMention', { chatRoomId: contractID, messageId })\n}\n\nfunction updateUnreadPosition ({ contractID, hash, createdDate } \n \n ) {\n sbp('state/vuex/commit', 'setChatRoomUnreadSince', {\n chatRoomId: contractID,\n messageId: hash,\n createdDate\n })\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/chatroom',\n metadata: {\n validate: objectOf({\n createdDate: string, // action created date\n username: string, // action creator\n identityContractID: string // action creator identityContractID\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n getters: {\n currentChatRoomState (state) {\n return state\n },\n chatRoomSettings (state, getters) {\n return getters.currentChatRoomState.settings || {}\n },\n chatRoomAttributes (state, getters) {\n return getters.currentChatRoomState.attributes || {}\n },\n chatRoomUsers (state, getters) {\n return getters.currentChatRoomState.users || {}\n },\n chatRoomLatestMessages (state, getters) {\n return getters.currentChatRoomState.messages || []\n }\n },\n actions: {\n // This is the constructor of Chat contract\n 'gi.contracts/chatroom': {\n validate: objectOf({\n attributes: chatRoomAttributesType\n }),\n process ({ meta, data }, { state }) {\n const initialState = merge({\n settings: {\n actionsPerPage: CHATROOM_ACTIONS_PER_PAGE,\n messagesPerPage: CHATROOM_MESSAGES_PER_PAGE,\n maxNameLength: CHATROOM_NAME_LIMITS_IN_CHARS,\n maxDescriptionLength: CHATROOM_DESCRIPTION_LIMITS_IN_CHARS\n },\n attributes: {\n creator: meta.username,\n deletedDate: null,\n archivedDate: null\n },\n users: {},\n messages: []\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/chatroom/join': {\n validate: objectOf({\n username: string // username of joining member\n }),\n process ({ data, meta, hash }, { state }) {\n const { username } = data\n if (!state.saveMessage && state.users[username]) {\n // this can happen when we're logging in on another machine, and also in other circumstances\n console.warn('Can not join the chatroom which you are already part of')\n return\n }\n\n Vue.set(state.users, username, { joinedDate: meta.createdDate })\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = username === meta.username ? MESSAGE_NOTIFICATIONS.JOIN_MEMBER : MESSAGE_NOTIFICATIONS.ADD_MEMBER\n const notificationData = createNotificationData(\n notificationType,\n notificationType === MESSAGE_NOTIFICATIONS.ADD_MEMBER ? { username } : {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM') || // Join by himself or Login in another device\n sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID') === contractID) { // Be added by another\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/rename': {\n validate: objectOf({\n name: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'name', data.name)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_NAME, {})\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/changeDescription': {\n validate: objectOf({\n description: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'description', data.description)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(\n MESSAGE_NOTIFICATIONS.UPDATE_DESCRIPTION, {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/leave': {\n validate: objectOf({\n username: optional(string), // coming from the gi.contracts/group/leaveChatRoom\n member: string // username to be removed\n }),\n process ({ data, meta, hash }, { state }) {\n const { member } = data\n const isKicked = data.username && member !== data.username\n if (!state.saveMessage && !state.users[member]) {\n throw new Error(`Can not leave the chatroom which ${member} are not part of`)\n }\n Vue.delete(state.users, member)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = !isKicked ? MESSAGE_NOTIFICATIONS.LEAVE_MEMBER : MESSAGE_NOTIFICATIONS.KICK_MEMBER\n const notificationData = createNotificationData(notificationType, isKicked ? { username: member } : {})\n const newMessage = createMessage({\n meta: isKicked ? meta : { ...meta, username: member },\n hash,\n data: notificationData,\n state\n })\n state.messages.push(newMessage)\n },\n sideEffect ({ data, hash, contractID, meta }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (data.member === rootState.loggedIn.username) {\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/delete': {\n validate: (data, { state, meta }) => {\n if (state.attributes.creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state, rootState }) {\n Vue.set(state.attributes, 'deletedDate', meta.createdDate)\n for (const username in state.users) {\n Vue.delete(state.users, username)\n }\n },\n sideEffect ({ meta, contractID }, { state }) {\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n },\n 'gi.contracts/chatroom/addMessage': {\n validate: messageType,\n process ({ data, meta, hash }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const pendingMsg = state.messages.find(msg => msg.id === hash && msg.pending)\n if (pendingMsg) {\n delete pendingMsg.pending\n } else {\n state.messages.push(createMessage({ meta, data, hash, state }))\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { state, getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const newMessage = createMessage({ meta, data, hash, state })\n const mentions = makeMentionFromUsername(me)\n if (data.type === MESSAGE_TYPES.TEXT &&\n (newMessage.text.includes(mentions.me) || newMessage.text.includes(mentions.all))) {\n addMention({\n contractID,\n messageId: newMessage.id,\n datetime: newMessage.datetime,\n text: newMessage.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n }\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/editMessage': {\n validate: (data, { state, meta }) => {\n objectOf({\n id: string,\n createdDate: string,\n text: string\n })(data)\n // TODO: Actually NOT SURE it's needed to check if the meta.username === message.from\n // there is no messagess in vuex state\n // to check if the meta.username is creator seems like too heavy\n },\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0 && meta.username === state.messages[msgIndex].from) {\n state.messages[msgIndex].text = data.text\n state.messages[msgIndex].updatedDate = meta.createdDate\n if (state.saveMessage && state.messages[msgIndex].pending) {\n delete state.messages[msgIndex].pending\n }\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const isAlreadyAdded = rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)\n const mentions = makeMentionFromUsername(me)\n const isIncludeMention = data.text.includes(mentions.me) || data.text.includes(mentions.all)\n if (!isAlreadyAdded && isIncludeMention) {\n addMention({\n contractID,\n messageId: data.id,\n /*\n * the following datetime is the time when the message(which made mention) is created\n * the reason why it is it instead of datetime when the mention created is because\n * it is compared to the datetime of other messages when user scrolls\n * to decide if it should be removed from the list of mentions or not\n */\n datetime: data.createdDate,\n text: data.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n } else if (isAlreadyAdded && !isIncludeMention) {\n deleteMention({ contractID, messageId: data.id })\n }\n }\n },\n 'gi.contracts/chatroom/deleteMessage': {\n validate: objectOf({\n id: string\n }),\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0) {\n state.messages.splice(msgIndex, 1)\n }\n // filter replied messages and check if the current message is original\n for (const message of state.messages) {\n if (message.replyingMessage?.id === data.id) {\n message.replyingMessage.id = null\n message.replyingMessage.text = 'Original message was removed.'\n }\n }\n },\n sideEffect ({ data, contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (rootState.chatRoomScrollPosition[contractID] === data.id) {\n sbp('state/vuex/commit', 'setChatRoomScrollPosition', {\n chatRoomId: contractID, messageId: null\n })\n }\n\n if (rootState.chatRoomUnread[contractID].since.messageId === data.id) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadSince', {\n chatRoomId: contractID,\n deletedDate: meta.createdDate\n })\n }\n\n if (me === meta.username) {\n return\n }\n if (rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)) {\n deleteMention({ contractID, messageId: data.id })\n }\n\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/makeEmotion': {\n validate: objectOf({\n id: string,\n emoticon: string\n }),\n process ({ data, meta, contractID }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const { id, emoticon } = data\n const msgIndex = findMessageIdx(id, state.messages)\n if (msgIndex >= 0) {\n let emoticons = cloneDeep(state.messages[msgIndex].emoticons || {})\n if (emoticons[emoticon]) {\n const alreadyAdded = emoticons[emoticon].indexOf(meta.username)\n if (alreadyAdded >= 0) {\n emoticons[emoticon].splice(alreadyAdded, 1)\n if (!emoticons[emoticon].length) {\n delete emoticons[emoticon]\n if (!Object.keys(emoticons).length) {\n emoticons = null\n }\n }\n } else {\n emoticons[emoticon].push(meta.username)\n }\n } else {\n emoticons[emoticon] = [meta.username]\n }\n if (emoticons) {\n Vue.set(state.messages[msgIndex], 'emoticons', emoticons)\n } else {\n Vue.delete(state.messages[msgIndex], 'emoticons')\n }\n }\n },\n sideEffect ({ contractID, hash }) {\n emitMessageEvent({ contractID, hash })\n }\n }\n }\n})\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/group.js b/test/contracts/group.js index 33146eda3d..f152ac509e 100644 --- a/test/contracts/group.js +++ b/test/contracts/group.js @@ -432,14 +432,14 @@ var objectOf = (typeObj, _scope = "Object") => { } const undefAttr = typeAttrs.find((property) => { const propertyTypeFn = typeObj[property]; - return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); }); if (undefAttr) { throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); } const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { const typeFn = typeObj[key]; - if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { return Object.assign(acc, {}); } else { return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); @@ -448,7 +448,10 @@ var objectOf = (typeObj, _scope = "Object") => { return typeAttrs.reduce(reducer, {}); } object2.type = () => { - const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); return `{| ${props.join(",\n ")} |}`; @@ -1539,6 +1542,7 @@ module_default("chelonia/defineContract", { objectOf({ member: string, reason: optional(string), + automated: optional(boolean), proposalHash: optional(string), proposalPayload: optional(objectOf({ secret: string diff --git a/test/contracts/group.js.map b/test/contracts/group.js.map index bdee49dc70..a49aacc94f 100644 --- a/test/contracts/group.js.map +++ b/test/contracts/group.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/common/errors.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/shared/voting/rules.js", "../../frontend/model/contracts/shared/time.js", "../../frontend/model/contracts/shared/voting/proposals.js", "../../frontend/model/contracts/shared/payments/index.js", "../../frontend/model/contracts/shared/distribution/mincome-proportional.js", "../../frontend/model/contracts/shared/distribution/payments-minimizer.js", "../../frontend/model/contracts/shared/currencies.js", "../../frontend/model/contracts/shared/distribution/distribution.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/group.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nexport class GIErrorIgnoreAndBan extends Error {\n // ugly boilerplate because JavaScript is stupid\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types\n constructor (...params ) {\n super(...params)\n this.name = 'GIErrorIgnoreAndBan'\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n\n// Used to throw human readable errors on UI.\nexport class GIErrorUIRuntimeError extends Error {\n constructor (...params ) {\n super(...params)\n // this.name = this.constructor.name\n this.name = 'GIErrorUIRuntimeError' // string literal so minifier doesn't overwrite\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n", "'use strict'\n\n \n \n \n \n\nexport default function mincomeProportional (haveNeeds ) {\n let totalHave = 0\n let totalNeed = 0\n const havers = []\n const needers = []\n for (const haveNeed of haveNeeds) {\n if (haveNeed.haveNeed > 0) {\n havers.push(haveNeed)\n totalHave += haveNeed.haveNeed\n } else if (haveNeed.haveNeed < 0) {\n needers.push(haveNeed)\n totalNeed += Math.abs(haveNeed.haveNeed)\n }\n }\n const totalPercent = Math.min(1, totalNeed / totalHave)\n const payments = []\n for (const haver of havers) {\n const distributionAmount = totalPercent * haver.haveNeed\n for (const needer of needers) {\n const belowPercentage = Math.abs(needer.haveNeed) / totalNeed\n payments.push({\n amount: distributionAmount * belowPercentage,\n from: haver.name,\n to: needer.name\n })\n }\n }\n return payments\n}\n", "'use strict'\n\n// greedy algorithm responsible for \"balancing\" payments\n// such that the least number of payments are made.\nexport default function minimizeTotalPaymentsCount (\n distribution \n) {\n const neederTotalReceived = {}\n const haverTotalHave = {}\n const haversSorted = []\n const needersSorted = []\n const minimizedDistribution = []\n for (const todo of distribution) {\n neederTotalReceived[todo.to] = (neederTotalReceived[todo.to] || 0) + todo.amount\n haverTotalHave[todo.from] = (haverTotalHave[todo.from] || 0) + todo.amount\n }\n for (const name in haverTotalHave) {\n haversSorted.push({ name, amount: haverTotalHave[name] })\n }\n for (const name in neederTotalReceived) {\n needersSorted.push({ name, amount: neederTotalReceived[name] })\n }\n // sort haves and needs: greatest to least\n haversSorted.sort((a, b) => b.amount - a.amount)\n needersSorted.sort((a, b) => b.amount - a.amount)\n while (haversSorted.length > 0 && needersSorted.length > 0) {\n const mostHaver = haversSorted.pop()\n const mostNeeder = needersSorted.pop()\n const diff = mostHaver.amount - mostNeeder.amount\n if (diff < 0) {\n // we used up everything the haver had\n minimizedDistribution.push({ amount: mostHaver.amount, from: mostHaver.name, to: mostNeeder.name })\n mostNeeder.amount -= mostHaver.amount\n needersSorted.push(mostNeeder)\n } else if (diff > 0) {\n // we completely filled up the needer's need and still have some left over\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n mostHaver.amount -= mostNeeder.amount\n haversSorted.push(mostHaver)\n } else {\n // a perfect match\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n }\n }\n return minimizedDistribution\n}\n", "'use strict'\n\n \n \n \n \n \n \n \n \n\n// https://github.com/okTurtles/group-income/issues/813#issuecomment-593680834\n// round all accounting to DECIMALS_MAX decimal places max to avoid consensus\n// issues that can arise due to different floating point values\n// at extreme precisions. If this becomes inadequate, instead of increasing\n// this value, switch to a different currency base, e.g. from BTC to mBTC.\nexport const DECIMALS_MAX = 8\n\nfunction commaToDots (value ) {\n // ex: \"1,55\" -> \"1.55\"\n return typeof value === 'string' ? value.replace(/,/, '.') : value.toString()\n}\n\nfunction isNumeric (nr ) {\n return !isNaN((nr ) - parseFloat(nr))\n}\n\nfunction isInDecimalsLimit (nr , decimalsMax ) {\n const decimals = nr.split('.')[1]\n return !decimals || decimals.length <= decimalsMax\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nfunction validateMincome (value , decimalsMax ) {\n const nr = commaToDots(value)\n return isNumeric(nr) && isInDecimalsLimit(nr, decimalsMax)\n}\n\nfunction decimalsOrInt (num , decimalsMax ) {\n // ex: 12.5 -> \"12.50\", but 250 -> \"250\"\n return num.toFixed(decimalsMax).replace(/\\.0+$/, '')\n}\n\nexport function saferFloat (value ) {\n // ex: 1.333333333333333333 -> 1.33333333\n return parseFloat(value.toFixed(DECIMALS_MAX))\n}\n\nexport function normalizeCurrency (value ) {\n // ex: \"1,333333333333333333\" -> 1.33333333\n return saferFloat(parseFloat(commaToDots(value)))\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nexport function mincomePositive (value ) {\n return parseFloat(commaToDots(value)) > 0\n}\n\nfunction makeCurrency (options) {\n const { symbol, symbolWithCode, decimalsMax, formatCurrency } = options\n return {\n symbol,\n symbolWithCode,\n decimalsMax,\n displayWithCurrency: (n ) => formatCurrency(decimalsOrInt(n, decimalsMax)),\n displayWithoutCurrency: (n ) => decimalsOrInt(n, decimalsMax),\n validate: (n ) => validateMincome(n, decimalsMax)\n }\n}\n\n// NOTE: if we needed for some reason, this could also be defined in\n// a json file that's read in and generates this object. For\n// example, that would allow the addition of currencies without\n// having to \"recompile\" a new version of the app.\nconst currencies = {\n USD: makeCurrency({\n symbol: '$',\n symbolWithCode: '$ USD',\n decimalsMax: 2,\n formatCurrency: amount => '$' + amount\n }),\n EUR: makeCurrency({\n symbol: '\u20AC',\n symbolWithCode: '\u20AC EUR',\n decimalsMax: 2,\n formatCurrency: amount => '\u20AC' + amount\n }),\n BTC: makeCurrency({\n symbol: '\u0243',\n symbolWithCode: '\u0243 BTC',\n decimalsMax: DECIMALS_MAX,\n formatCurrency: amount => amount + '\u0243'\n })\n}\n\nexport default currencies\n", "'use strict'\n\nimport mincomeProportional from './mincome-proportional.js'\nimport minimizeTotalPaymentsCount from './payments-minimizer.js'\nimport { cloneDeep } from '../giLodash.js'\nimport { saferFloat, DECIMALS_MAX } from '../currencies.js'\n\n \n\nconst tinyNum = 1 / Math.pow(10, DECIMALS_MAX)\n\nexport function unadjustedDistribution ({ haveNeeds = [], minimize = true } \n \n ) {\n const distribution = mincomeProportional(haveNeeds)\n return minimize ? minimizeTotalPaymentsCount(distribution) : distribution\n}\n\nexport function adjustedDistribution (\n { distribution, payments, dueOn } \n) {\n distribution = cloneDeep(distribution)\n // ensure the total is set because of how reduceDistribution works\n for (const todo of distribution) {\n todo.total = todo.amount\n }\n distribution = subtractDistributions(distribution, payments)\n // remove any todos for containing miniscule amounts\n // and pledgers who switched sides should have their todos removed\n .filter(todo => todo.amount >= tinyNum)\n for (const todo of distribution) {\n todo.amount = saferFloat(todo.amount)\n todo.total = saferFloat(todo.total)\n todo.partial = todo.total !== todo.amount\n todo.isLate = false\n todo.dueOn = dueOn\n }\n // TODO: add in latePayments to the end of the distribution\n // consider passing in latePayments\n return distribution\n}\n\n// Merges multiple payments between any combinations two of users:\nfunction reduceDistribution (payments ) {\n // Don't modify the payments list/object parameter in-place, as this is not intended:\n payments = cloneDeep(payments)\n for (let i = 0; i < payments.length; i++) {\n const paymentA = payments[i]\n for (let j = i + 1; j < payments.length; j++) {\n const paymentB = payments[j]\n\n // Were paymentA and paymentB between the same two users?\n if ((paymentA.from === paymentB.from && paymentA.to === paymentB.to) ||\n (paymentA.to === paymentB.from && paymentA.from === paymentB.to)) {\n // Add or subtract paymentB's amount to paymentA's amount, depending on the relative\n // direction of the two payments:\n paymentA.amount += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.amount\n paymentA.total += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.total\n // Remove paymentB from payments, and decrement the inner sentinal loop variable:\n payments.splice(j, 1)\n j--\n }\n }\n }\n return payments\n}\n\nfunction addDistributions (paymentsA , paymentsB ) {\n return reduceDistribution([...paymentsA, ...paymentsB])\n}\n\nfunction subtractDistributions (paymentsA , paymentsB ) {\n // Don't modify any payment list/objects parameters in-place, as this is not intended:\n paymentsB = cloneDeep(paymentsB)\n // Reverse the sign of the second operand's amounts so that the final addition is actually subtraction:\n for (const p of paymentsB) {\n p.amount *= -1\n p.total *= -1\n }\n return addDistributions(paymentsA, paymentsB)\n}\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, Errors, L } from '@common/common.js'\nimport votingRules, { ruleType, VOTE_FOR, VOTE_AGAINST, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './shared/voting/rules.js'\nimport proposals, { proposalType, proposalSettingsType, archiveProposal } from './shared/voting/proposals.js'\nimport {\n PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC, STATUS_OPEN, STATUS_CANCELLED, MAX_ARCHIVED_PROPOSALS, PROPOSAL_ARCHIVED,\n INVITE_INITIAL_CREATOR, INVITE_STATUS, PROFILE_STATUS, INVITE_EXPIRES_IN_DAYS\n} from './shared/constants.js'\nimport { paymentStatusType, paymentType, PAYMENT_COMPLETED } from './shared/payments/index.js'\nimport { merge, deepEqualJSONType, omit } from './shared/giLodash.js'\nimport { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, periodStampGivenDate, dateIsWithinPeriod, DAYS_MILLIS } from './shared/time.js'\nimport { unadjustedDistribution, adjustedDistribution } from './shared/distribution/distribution.js'\nimport currencies, { saferFloat } from './shared/currencies.js'\nimport { inviteType, chatRoomAttributesType } from './shared/types.js'\nimport { arrayOf, mapOf, objectOf, objectMaybeOf, optional, string, number, boolean, object, unionOf, tupleOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction vueFetchInitKV (obj , key , initialValue ) {\n let value = obj[key]\n if (!value) {\n Vue.set(obj, key, initialValue)\n value = obj[key]\n }\n return value\n}\n\nfunction initGroupProfile (contractID , joinedDate ) {\n return {\n globalUsername: '', // TODO: this? e.g. groupincome:greg / namecoin:bob / ens:alice\n contractID,\n joinedDate,\n nonMonetaryContributions: [],\n status: PROFILE_STATUS.ACTIVE,\n departedDate: null\n }\n}\n\nfunction initPaymentPeriod ({ getters }) {\n return {\n // this saved so that it can be used when creating a new payment\n initialCurrency: getters.groupMincomeCurrency,\n // TODO: should we also save the first period's currency exchange rate..?\n // all payments during the period use this to set their exchangeRate\n // see notes and code in groupIncomeAdjustedDistribution for details.\n // TODO: for the currency change proposal, have it update the mincomeExchangeRate\n // using .mincomeExchangeRate *= proposal.exchangeRate\n mincomeExchangeRate: 1, // modified by proposals to change mincome currency\n paymentsFrom: {}, // fromUser => toUser => Array\n // snapshot of adjusted distribution after each completed payment\n // yes, it is possible a payment began in one period and completed in another\n // in which case lastAdjustedDistribution for the previous period will be updated\n lastAdjustedDistribution: null,\n // snapshot of haveNeeds. made only when there are no payments\n haveNeedsSnapshot: null\n }\n}\n\n// NOTE: do not call any of these helper functions from within a getter b/c they modify state!\n\nfunction clearOldPayments ({ state, getters }) {\n const sortedPeriodKeys = Object.keys(state.paymentsByPeriod).sort()\n // save two periods worth of payments, max\n while (sortedPeriodKeys.length > 2) {\n const period = sortedPeriodKeys.shift()\n for (const paymentHash of getters.paymentHashesForPeriod(period)) {\n Vue.delete(state.payments, paymentHash)\n // TODO: archive the old payments in a sideEffect, not here\n }\n Vue.delete(state.paymentsByPeriod, period)\n }\n}\n\nfunction initFetchPeriodPayments ({ meta, state, getters }) {\n const period = getters.periodStampGivenDate(meta.createdDate)\n const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ getters }))\n clearOldPayments({ state, getters })\n return periodPayments\n}\n\n// this function is called each time a payment is completed or a user adjusts their income details.\n// TODO: call also when mincome is adjusted\nfunction updateCurrentDistribution ({ meta, state, getters }) {\n const curPeriodPayments = initFetchPeriodPayments({ meta, state, getters })\n const period = getters.periodStampGivenDate(meta.createdDate)\n const noPayments = Object.keys(curPeriodPayments.paymentsFrom).length === 0\n // update distributionDate if we've passed into the next period\n if (comparePeriodStamps(period, getters.groupSettings.distributionDate) > 0) {\n getters.groupSettings.distributionDate = period\n }\n // save haveNeeds if there are no payments or the haveNeeds haven't been saved yet\n if (noPayments || !curPeriodPayments.haveNeedsSnapshot) {\n curPeriodPayments.haveNeedsSnapshot = getters.haveNeedsForThisPeriod(period)\n }\n // if there are payments this period, save the adjusted distribution\n if (!noPayments) {\n updateAdjustedDistribution({ period, getters })\n }\n}\n\nfunction updateAdjustedDistribution ({ period, getters }) {\n const payments = getters.groupPeriodPayments[period]\n if (payments && payments.haveNeedsSnapshot) {\n const minimize = getters.groupSettings.minimizeDistribution\n payments.lastAdjustedDistribution = adjustedDistribution({\n distribution: unadjustedDistribution({ haveNeeds: payments.haveNeedsSnapshot, minimize }),\n payments: getters.paymentsForPeriod(period),\n dueOn: getters.dueDateForPeriod(period)\n }).filter(todo => {\n // only return todos for active members\n return getters.groupProfile(todo.to).status === PROFILE_STATUS.ACTIVE\n })\n }\n}\n\nfunction memberLeaves ({ username, dateLeft }, { meta, state, getters }) {\n state.profiles[username].status = PROFILE_STATUS.REMOVED\n state.profiles[username].departedDate = dateLeft\n // remove any todos for this member from the adjusted distribution\n updateCurrentDistribution({ meta, state, getters })\n}\n\nfunction isActionYoungerThanUser (actionMeta , userProfile ) {\n // A util function that checks if an action (or event) in a group occurred after a particular user joined a group.\n // This is used mostly for checking if a notification should be sent for that user or not.\n // e.g.) user-2 who joined a group later than user-1 (who is the creator of the group) doesn't need to receive\n // 'MEMBER_ADDED' notification for user-1.\n // In some situations, userProfile is undefined, for example, when inviteAccept is called in\n // certain situations. So we need to check for that here.\n return Boolean(userProfile) && compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/group',\n metadata: {\n validate: objectOf({\n createdDate: string,\n username: string,\n identityContractID: string\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n // TODO: We may want to get the time from the server instead of relying on\n // the client in case the client's clock isn't set correctly.\n // the only issue here is that it involves an async function...\n // See: https://github.com/okTurtles/group-income/issues/531\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n // These getters are restricted only to the contract's state.\n // Do not access state outside the contract state inside of them.\n // For example, if the getter you use tries to access `state.loggedIn`,\n // that will break the `latestContractState` function in state.js.\n // It is only safe to access state outside of the contract in a contract action's\n // `sideEffect` function (as long as it doesn't modify contract state)\n getters: {\n // we define `currentGroupState` here so that we can redefine it in state.js\n // so that we can re-use these getter definitions in state.js since they are\n // compatible with Vuex getter definitions.\n // Here `state` refers to the individual group contract's state, the equivalent\n // of `vuexRootState[someGroupContractID]`.\n currentGroupState (state) {\n return state\n },\n groupSettings (state, getters) {\n return getters.currentGroupState.settings || {}\n },\n groupProfile (state, getters) {\n return username => {\n const profiles = getters.currentGroupState.profiles\n return profiles && profiles[username]\n }\n },\n groupProfiles (state, getters) {\n const profiles = {}\n for (const username in (getters.currentGroupState.profiles || {})) {\n const profile = getters.groupProfile(username)\n if (profile.status === PROFILE_STATUS.ACTIVE) {\n profiles[username] = profile\n }\n }\n return profiles\n },\n groupMincomeAmount (state, getters) {\n return getters.groupSettings.mincomeAmount\n },\n groupMincomeCurrency (state, getters) {\n return getters.groupSettings.mincomeCurrency\n },\n periodStampGivenDate (state, getters) {\n return (recentDate ) => {\n if (typeof recentDate !== 'string') {\n recentDate = recentDate.toISOString()\n }\n const { distributionDate, distributionPeriodLength } = getters.groupSettings\n return periodStampGivenDate({\n recentDate,\n periodStart: distributionDate,\n periodLength: distributionPeriodLength\n })\n }\n },\n periodBeforePeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -len))\n }\n },\n periodAfterPeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), len))\n }\n },\n dueDateForPeriod (state, getters) {\n return (periodStamp ) => {\n return dateToPeriodStamp(\n addTimeToDate(\n dateFromPeriodStamp(getters.periodAfterPeriod(periodStamp)),\n -DAYS_MILLIS\n )\n )\n }\n },\n paymentTotalFromUserToUser (state, getters) {\n return (fromUser, toUser, periodStamp) => {\n const payments = getters.currentGroupState.payments\n const periodPayments = getters.groupPeriodPayments\n const { paymentsFrom, mincomeExchangeRate } = periodPayments[periodStamp] || {}\n // NOTE: @babel/plugin-proposal-optional-chaining would come in super-handy\n // here, but I couldn't get it to work with our linter. :(\n // https://github.com/babel/babel-eslint/issues/511\n const total = (((paymentsFrom || {})[fromUser] || {})[toUser] || []).reduce((a, hash) => {\n const payment = payments[hash]\n let { amount, exchangeRate, status } = payment.data\n if (status !== PAYMENT_COMPLETED) {\n return a\n }\n const paymentCreatedPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n // if this payment is from a previous period, then make sure to take into account\n // any proposals that passed in between the payment creation and the payment\n // completion that modified the group currency by multiplying both period's\n // exchange rates\n if (periodStamp !== paymentCreatedPeriodStamp) {\n if (paymentCreatedPeriodStamp !== getters.periodBeforePeriod(periodStamp)) {\n console.warn(`paymentTotalFromUserToUser: super old payment shouldn't exist, ignoring! (curPeriod=${periodStamp})`, JSON.stringify(payment))\n return a\n }\n exchangeRate *= periodPayments[paymentCreatedPeriodStamp].mincomeExchangeRate\n }\n return a + (amount * exchangeRate * mincomeExchangeRate)\n }, 0)\n return saferFloat(total)\n }\n },\n paymentHashesForPeriod (state, getters) {\n return (periodStamp) => {\n const periodPayments = getters.groupPeriodPayments[periodStamp]\n if (periodPayments) {\n let hashes = []\n const { paymentsFrom } = periodPayments\n for (const fromUser in paymentsFrom) {\n for (const toUser in paymentsFrom[fromUser]) {\n hashes = hashes.concat(paymentsFrom[fromUser][toUser])\n }\n }\n return hashes\n }\n }\n },\n groupMembersByUsername (state, getters) {\n return Object.keys(getters.groupProfiles)\n },\n groupMembersCount (state, getters) {\n return getters.groupMembersByUsername.length\n },\n groupMembersPending (state, getters) {\n const invites = getters.currentGroupState.invites\n const pendingMembers = {}\n for (const inviteId in invites) {\n const invite = invites[inviteId]\n if (\n invite.status === INVITE_STATUS.VALID &&\n invite.creator !== INVITE_INITIAL_CREATOR\n ) {\n pendingMembers[invites[inviteId].invitee] = {\n invitedBy: invites[inviteId].creator,\n expires: invite.expires\n }\n }\n }\n return pendingMembers\n },\n groupShouldPropose (state, getters) {\n return getters.groupMembersCount >= 3\n },\n groupProposalSettings (state, getters) {\n return (proposalType = PROPOSAL_GENERIC) => {\n return getters.groupSettings.proposals[proposalType]\n }\n },\n groupCurrency (state, getters) {\n const mincomeCurrency = getters.groupMincomeCurrency\n return mincomeCurrency && currencies[mincomeCurrency]\n },\n groupMincomeFormatted (state, getters) {\n return getters.withGroupCurrency?.(getters.groupMincomeAmount)\n },\n groupMincomeSymbolWithCode (state, getters) {\n return getters.groupCurrency?.symbolWithCode\n },\n groupPeriodPayments (state, getters) {\n // note: a lot of code expects this to return an object, so keep the || {} below\n return getters.currentGroupState.paymentsByPeriod || {}\n },\n withGroupCurrency (state, getters) {\n // TODO: If this group has no defined mincome currency, not even a default one like\n // USD, then calling this function is probably an error which should be reported.\n // Just make sure the UI doesn't break if an exception is thrown, since this is\n // bound to the UI in some location.\n return getters.groupCurrency?.displayWithCurrency\n },\n getChatRooms (state, getters) {\n return getters.currentGroupState.chatRooms\n },\n generalChatRoomId (state, getters) {\n return getters.currentGroupState.generalChatRoomId\n },\n // getter is named haveNeedsForThisPeriod instead of haveNeedsForPeriod because it uses\n // getters.groupProfiles - and that is always based on the most recent values. we still\n // pass in the current period because it's used to set the \"when\" property\n haveNeedsForThisPeriod (state, getters) {\n return (currentPeriod ) => {\n // NOTE: if we ever switch back to the \"real-time\" adjusted distribution algorithm,\n // make sure that this function also handles userExitsGroupEvent\n const groupProfiles = getters.groupProfiles // TODO: these should use the haveNeeds for the specific period's distribution period\n const haveNeeds = []\n for (const username in groupProfiles) {\n const { incomeDetailsType, joinedDate } = groupProfiles[username]\n if (incomeDetailsType) {\n const amount = groupProfiles[username][incomeDetailsType]\n const haveNeed = incomeDetailsType === 'incomeAmount' ? amount - getters.groupMincomeAmount : amount\n // construct 'when' this way in case we ever use a pro-rated algorithm\n let when = dateFromPeriodStamp(currentPeriod).toISOString()\n if (dateIsWithinPeriod({\n date: joinedDate,\n periodStart: currentPeriod,\n periodLength: getters.groupSettings.distributionPeriodLength\n })) {\n when = joinedDate\n }\n haveNeeds.push({ name: username, haveNeed, when })\n }\n }\n return haveNeeds\n }\n },\n paymentsForPeriod (state, getters) {\n return (periodStamp) => {\n const hashes = getters.paymentHashesForPeriod(periodStamp)\n const events = []\n if (hashes && hashes.length > 0) {\n const payments = getters.currentGroupState.payments\n for (const paymentHash of hashes) {\n const payment = payments[paymentHash]\n if (payment.data.status === PAYMENT_COMPLETED) {\n events.push({\n from: payment.meta.username,\n to: payment.data.toUser,\n hash: paymentHash,\n amount: payment.data.amount,\n isLate: !!payment.data.isLate,\n when: payment.data.completedDate\n })\n }\n }\n }\n return events\n }\n }\n // distributionEventsForMonth (state, getters) {\n // return (monthstamp) => {\n // // NOTE: if we ever switch back to the \"real-time\" adjusted distribution\n // // algorithm, make sure that this function also handles userExitsGroupEvent\n // const distributionEvents = getters.haveNeedEventsForMonth(monthstamp)\n // const paymentEvents = getters.paymentEventsForMonth(monthstamp)\n // distributionEvents.splice(distributionEvents.length, 0, paymentEvents)\n // return distributionEvents.sort((a, b) => compareISOTimestamps(a.data.when, b.data.when))\n // }\n // }\n },\n // NOTE: All mutations must be atomic in their edits of the contract state.\n // THEY ARE NOT to farm out any further mutations through the async actions!\n actions: {\n // this is the constructor\n 'gi.contracts/group': {\n validate: objectMaybeOf({\n invites: mapOf(string, inviteType),\n settings: objectMaybeOf({\n // TODO: add 'groupPubkey'\n groupName: string,\n groupPicture: string,\n sharedValues: string,\n mincomeAmount: number,\n mincomeCurrency: string,\n distributionDate: isPeriodStamp,\n distributionPeriodLength: number,\n minimizeDistribution: boolean,\n proposals: objectOf({\n [PROPOSAL_INVITE_MEMBER]: proposalSettingsType,\n [PROPOSAL_REMOVE_MEMBER]: proposalSettingsType,\n [PROPOSAL_GROUP_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_GENERIC]: proposalSettingsType\n })\n })\n }),\n process ({ data, meta }, { state, getters }) {\n // TODO: checkpointing: https://github.com/okTurtles/group-income/issues/354\n const initialState = merge({\n payments: {},\n paymentsByPeriod: {},\n invites: {},\n proposals: {}, // hashes => {} TODO: this, see related TODOs in GroupProposal\n settings: {\n groupCreator: meta.username,\n distributionPeriodLength: 30 * DAYS_MILLIS,\n inviteExpiryOnboarding: INVITE_EXPIRES_IN_DAYS.ON_BOARDING,\n inviteExpiryProposal: INVITE_EXPIRES_IN_DAYS.PROPOSAL\n },\n profiles: {\n [meta.username]: initGroupProfile(meta.identityContractID, meta.createdDate)\n },\n chatRooms: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n initFetchPeriodPayments({ meta, state, getters })\n }\n },\n 'gi.contracts/group/payment': {\n validate: objectMaybeOf({\n // TODO: how to handle donations to okTurtles?\n // TODO: how to handle payments to groups or users outside of this group?\n toUser: string,\n amount: number,\n currencyFromTo: tupleOf(string, string), // must be one of the keys in currencies.js (e.g. USD, EUR, etc.) TODO: handle old clients not having one of these keys, see OP_PROTOCOL_UPGRADE https://github.com/okTurtles/group-income/issues/603\n // multiply 'amount' by 'exchangeRate', which must always be\n // based on the initialCurrency of the period in which this payment was created.\n // it is then further multiplied by the period's 'mincomeExchangeRate', which\n // is modified if any proposals pass to change the mincomeCurrency\n exchangeRate: number,\n txid: string,\n status: paymentStatusType,\n paymentType: paymentType,\n details: optional(object),\n memo: optional(string)\n }),\n process ({ data, meta, hash }, { state, getters }) {\n if (data.status === PAYMENT_COMPLETED) {\n console.error(`payment: payment ${hash} cannot have status = 'completed'!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('payments cannot be instantly completed!')\n }\n Vue.set(state.payments, hash, {\n data: {\n ...data,\n groupMincome: getters.groupMincomeAmount\n },\n meta,\n history: [[meta.createdDate, hash]]\n })\n const { paymentsFrom } = initFetchPeriodPayments({ meta, state, getters })\n const fromUser = vueFetchInitKV(paymentsFrom, meta.username, {})\n const toUser = vueFetchInitKV(fromUser, data.toUser, [])\n toUser.push(hash)\n // TODO: handle completed payments here too! (for manual payment support)\n }\n },\n 'gi.contracts/group/paymentUpdate': {\n validate: objectMaybeOf({\n paymentHash: string,\n updatedProperties: objectMaybeOf({\n status: paymentStatusType,\n details: object,\n memo: string\n })\n }),\n process ({ data, meta, hash }, { state, getters }) {\n // TODO: we don't want to keep a history of all payments in memory all the time\n // https://github.com/okTurtles/group-income/issues/426\n const payment = state.payments[data.paymentHash]\n // TODO: move these types of validation errors into the validate function so\n // that they can be done before sending as well as upon receiving\n if (!payment) {\n console.error(`paymentUpdate: no payment ${data.paymentHash}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate without existing payment')\n }\n // if the payment is being modified by someone other than the person who sent or received it, throw an exception\n if (meta.username !== payment.meta.username && meta.username !== payment.data.toUser) {\n console.error(`paymentUpdate: bad username ${meta.username} != ${payment.meta.username} != ${payment.data.username}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate from bad user!')\n }\n payment.history.push([meta.createdDate, hash])\n merge(payment.data, data.updatedProperties)\n // we update \"this period\"'s snapshot 'lastAdjustedDistribution' on each completed payment\n if (data.updatedProperties.status === PAYMENT_COMPLETED) {\n payment.data.completedDate = meta.createdDate\n // update the current distribution unless this update is for a payment from the previous period\n const updatePeriodStamp = getters.periodStampGivenDate(meta.createdDate)\n const paymentPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n if (comparePeriodStamps(updatePeriodStamp, paymentPeriodStamp) > 0) {\n updateAdjustedDistribution({ period: paymentPeriodStamp, getters })\n } else {\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n }\n },\n 'gi.contracts/group/proposal': {\n validate: (data, { state, meta }) => {\n objectOf({\n proposalType: proposalType,\n proposalData: object, // data for Vue widgets\n votingRule: ruleType,\n expires_date_ms: number // calculate by grabbing proposal expiry from group properties and add to `meta.createdDate`\n })(data)\n\n const dataToCompare = omit(data.proposalData, ['reason'])\n\n // Validate this isn't a duplicate proposal\n for (const hash in state.proposals) {\n const prop = state.proposals[hash]\n if (prop.status !== STATUS_OPEN || prop.data.proposalType !== data.proposalType) {\n continue\n }\n\n if (deepEqualJSONType(omit(prop.data.proposalData, ['reason']), dataToCompare)) {\n throw new TypeError(L('There is an identical open proposal.'))\n }\n\n // TODO - verify if type of proposal already exists (SETTING_CHANGE).\n }\n },\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.proposals, hash, {\n data,\n meta,\n votes: { [meta.username]: VOTE_FOR },\n status: STATUS_OPEN,\n payload: null // set later by group/proposalVote\n })\n // TODO: save all proposals disk so that we only keep open proposals in memory\n // TODO: create a global timer to auto-pass/archive expired votes\n // make sure to set that proposal's status as STATUS_EXPIRED if it's expired\n },\n sideEffect ({ contractID, meta, data }, { getters }) {\n const { loggedIn } = sbp('state/vuex/state')\n const typeToSubTypeMap = {\n [PROPOSAL_INVITE_MEMBER]: 'ADD_MEMBER',\n [PROPOSAL_REMOVE_MEMBER]: 'REMOVE_MEMBER',\n [PROPOSAL_GROUP_SETTING_CHANGE]: 'CHANGE_MINCOME',\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: 'CHANGE_VOTING_RULE',\n [PROPOSAL_GENERIC]: 'GENERIC'\n }\n\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'NEW_PROPOSAL', {\n groupID: contractID,\n creator: meta.username,\n subtype: typeToSubTypeMap[data.proposalType]\n })\n }\n }\n },\n 'gi.contracts/group/proposalVote': {\n validate: objectOf({\n proposalHash: string,\n vote: string,\n passPayload: optional(unionOf(object, string)) // TODO: this, somehow we need to send an OP_KEY_ADD GIMessage to add a generated once-only writeonly message public key to the contract, and (encrypted) include the corresponding invite link, also, we need all clients to verify that this message/operation was valid to prevent a hacked client from adding arbitrary OP_KEY_ADD messages, and automatically ban anyone generating such messages\n }),\n process (message, { state }) {\n const { data, hash, meta } = message\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalVote: no proposal for ${data.proposalHash}!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n }\n Vue.set(proposal.votes, meta.username, data.vote)\n // TODO: handle vote pass/fail\n // check if proposal is expired, if so, ignore (but log vote)\n if (new Date(meta.createdDate).getTime() > proposal.data.expires_date_ms) {\n console.warn('proposalVote: vote on expired proposal!', { proposal, data, meta })\n // TODO: display warning or something\n return\n }\n // see if this is a deciding vote\n const result = votingRules[proposal.data.votingRule](state, proposal.data.proposalType, proposal.votes)\n if (result === VOTE_FOR || result === VOTE_AGAINST) {\n // handles proposal pass or fail, will update proposal.status accordingly\n proposals[proposal.data.proposalType][result](state, message)\n Vue.set(proposal, 'dateClosed', meta.createdDate)\n }\n },\n sideEffect ({ contractID, data, meta }, { state, getters }) {\n const proposal = state.proposals[data.proposalHash]\n const { loggedIn } = sbp('state/vuex/state')\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (proposal?.dateClosed &&\n isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'PROPOSAL_CLOSED', {\n groupID: contractID,\n creator: meta.username,\n proposalStatus: proposal.status\n })\n }\n }\n },\n 'gi.contracts/group/proposalCancel': {\n validate: objectOf({\n proposalHash: string\n }),\n process ({ data, meta, contractID }, { state }) {\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalCancel: no proposal for ${data.proposalHash}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n } else if (proposal.meta.username !== meta.username) {\n console.error(`proposalCancel: proposal ${data.proposalHash} belongs to ${proposal.meta.username} not ${meta.username}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalWithdraw for wrong user!')\n }\n Vue.set(proposal, 'status', STATUS_CANCELLED)\n archiveProposal({ state, proposalHash: data.proposalHash, proposal, contractID })\n }\n },\n 'gi.contracts/group/removeMember': {\n validate: (data, { state, getters, meta }) => {\n objectOf({\n member: string, // username to remove\n reason: optional(string),\n // In case it happens in a big group (by proposal)\n // we need to validate the associated proposal.\n proposalHash: optional(string),\n proposalPayload: optional(objectOf({\n secret: string // NOTE: simulate the OP_KEY_* stuff for now\n }))\n })(data)\n\n const memberToRemove = data.member\n const membersCount = getters.groupMembersCount\n\n if (!state.profiles[memberToRemove]) {\n throw new TypeError(L('Not part of the group.'))\n }\n if (membersCount === 1 || memberToRemove === meta.username) {\n throw new TypeError(L('Cannot remove yourself.'))\n }\n\n if (membersCount < 3) {\n // In a small group only the creator can remove someone\n // TODO: check whether meta.username has required admin permissions\n if (meta.username !== state.settings.groupCreator) {\n throw new TypeError(L('Only the group creator can remove members.'))\n }\n } else {\n // In a big group a removal can only happen through a proposal\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // TODO this\n throw new TypeError(L('Admin credentials needed and not implemented yet.'))\n }\n\n if (!proposal.payload || proposal.payload.secret !== data.proposalPayload.secret) {\n throw new TypeError(L('Invalid associated proposal.'))\n }\n }\n },\n process ({ data, meta }, { state, getters }) {\n memberLeaves(\n { username: data.member, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n },\n sideEffect ({ data, meta, contractID }, { state, getters }) {\n const rootState = sbp('state/vuex/state')\n const contracts = rootState.contracts || {}\n const { username } = rootState.loggedIn\n\n if (data.member === username) {\n // If this member is re-joining the group, ignore the rest\n // so the member doesn't remove themself again.\n if (sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n return\n }\n\n const groupIdToSwitch = Object.keys(contracts)\n .find(cID => contracts[cID].type === 'gi.contracts/group' &&\n cID !== contractID && rootState[cID].settings) || null\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {})\n sbp('state/vuex/commit', 'setCurrentGroupId', groupIdToSwitch)\n // we can't await on this in here, because it will cause a deadlock, since Chelonia processes\n // this sideEffect on the eventqueue for this contractID, and /remove uses that same eventqueue\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown by /remove ${contractID}:`, e)\n })\n // this looks crazy, but doing this was necessary to fix a race condition in the\n // group-member-removal Cypress tests where due to the ordering of asynchronous events\n // we were getting the same latestHash upon re-logging in for test \"user2 rejoins groupA\".\n // We add it to the same queue as '/remove' above gets run on so that it is run after\n // contractID is removed. See also comments in 'gi.actions/identity/login'.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/saveOurLoginState'])\n .then(function () {\n const router = sbp('controller/router')\n const switchFrom = router.currentRoute.path\n const switchTo = groupIdToSwitch ? '/dashboard' : '/'\n if (switchFrom !== '/join' && switchFrom !== switchTo) {\n router.push({ path: switchTo }).catch(console.warn)\n }\n }).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown during queueEvent to ${contractID} by saveOurLoginState:`, e)\n })\n // TODO - #828 remove other group members contracts if applicable\n } else {\n const myProfile = getters.groupProfile(username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n const memberRemovedThemselves = data.member === meta.username\n\n sbp('gi.notifications/emit', // emit a notification for a member removal.\n memberRemovedThemselves ? 'MEMBER_LEFT' : 'MEMBER_REMOVED',\n {\n groupID: contractID,\n username: memberRemovedThemselves ? meta.username : data.member\n })\n }\n // TODO - #828 remove the member contract if applicable.\n // problem is, if they're in another group we're also a part of, or if we\n // have a DM with them, we don't want to do this. may need to use manual reference counting\n // sbp('chelonia/contract/release', getters.groupProfile(data.member).contractID)\n }\n // TODO - #850 verify open proposals and see if they need some re-adjustment.\n }\n },\n 'gi.contracts/group/removeOurselves': {\n validate: objectMaybeOf({\n reason: string\n }),\n process ({ data, meta, contractID }, { state, getters }) {\n memberLeaves(\n { username: meta.username, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', {\n meta,\n data: { member: meta.username, reason: data.reason || '' },\n contractID\n }]\n )\n }\n },\n 'gi.contracts/group/invite': {\n validate: inviteType,\n process ({ data, meta }, { state }) {\n Vue.set(state.invites, data.inviteSecret, data)\n }\n },\n 'gi.contracts/group/inviteAccept': {\n validate: objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n }),\n process ({ data, meta }, { state }) {\n console.debug('inviteAccept:', data, state.invites)\n const invite = state.invites[data.inviteSecret]\n if (invite.status !== INVITE_STATUS.VALID) {\n console.error(`inviteAccept: invite for ${meta.username} is: ${invite.status}`)\n return\n }\n Vue.set(invite.responses, meta.username, true)\n if (Object.keys(invite.responses).length === invite.quantity) {\n invite.status = INVITE_STATUS.USED\n }\n // TODO: ensure `meta.username` is unique for the lifetime of the username\n // since we are making it possible for the same username to leave and\n // rejoin the group. All of their past posts will be re-associated with\n // them upon re-joining.\n Vue.set(state.profiles, meta.username, initGroupProfile(meta.identityContractID, meta.createdDate))\n // If we're triggered by handleEvent in state.js (and not latestContractState)\n // then the asynchronous sideEffect function will get called next\n // and we will subscribe to this new user's identity contract\n },\n // !! IMPORANT!!\n // Actions here MUST NOT modify contract state!\n // They MUST NOT call 'commit'!\n // They should only coordinate the actions of outside contracts.\n // Otherwise `latestContractState` and `handleEvent` will not produce same state!\n async sideEffect ({ meta, contractID }, { state }) {\n const { loggedIn } = sbp('state/vuex/state')\n const { profiles = {} } = state\n\n // TODO: per #257 this will ,have to be encompassed in a recoverable transaction\n // however per #610 that might be handled in handleEvent (?), or per #356 might not be needed\n if (meta.username === loggedIn.username) {\n // we're the person who just accepted the group invite\n // so subscribe to founder's IdentityContract & everyone else's\n for (const name in profiles) {\n if (name !== loggedIn.username) {\n await sbp('chelonia/contract/sync', profiles[name].contractID)\n }\n }\n } else {\n const myProfile = profiles[loggedIn.username]\n // we're an existing member of the group getting notified that a\n // new member has joined, so subscribe to their identity contract\n await sbp('chelonia/contract/sync', meta.identityContractID)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'MEMBER_ADDED', { // emit a notification for a member addition.\n groupID: contractID,\n username: meta.username\n })\n }\n }\n }\n },\n 'gi.contracts/group/inviteRevoke': {\n validate: (data, { state, meta }) => {\n objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n })(data)\n\n if (!state.invites[data.inviteSecret]) {\n throw new TypeError(L('The link does not exist.'))\n }\n },\n process ({ data, meta }, { state }) {\n const invite = state.invites[data.inviteSecret]\n Vue.set(invite, 'status', INVITE_STATUS.REVOKED)\n }\n },\n 'gi.contracts/group/updateSettings': {\n // OPTIMIZE: Make this custom validation function\n // reusable accross other future validators\n validate: objectMaybeOf({\n groupName: x => typeof x === 'string',\n groupPicture: x => typeof x === 'string',\n sharedValues: x => typeof x === 'string',\n mincomeAmount: x => typeof x === 'number' && x > 0,\n mincomeCurrency: x => typeof x === 'string'\n }),\n process ({ meta, data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/group/groupProfileUpdate': {\n validate: objectMaybeOf({\n incomeDetailsType: x => ['incomeAmount', 'pledgeAmount'].includes(x),\n incomeAmount: x => typeof x === 'number' && x >= 0,\n pledgeAmount: x => typeof x === 'number' && x >= 0,\n nonMonetaryAdd: string,\n nonMonetaryEdit: objectOf({\n replace: string,\n with: string\n }),\n nonMonetaryRemove: string,\n paymentMethods: arrayOf(\n objectOf({\n name: string,\n value: string\n })\n )\n }),\n process ({ data, meta }, { state, getters }) {\n const groupProfile = state.profiles[meta.username]\n const nonMonetary = groupProfile.nonMonetaryContributions\n for (const key in data) {\n const value = data[key]\n switch (key) {\n case 'nonMonetaryAdd':\n nonMonetary.push(value)\n break\n case 'nonMonetaryRemove':\n nonMonetary.splice(nonMonetary.indexOf(value), 1)\n break\n case 'nonMonetaryEdit':\n nonMonetary.splice(nonMonetary.indexOf(value.replace), 1, value.with)\n break\n default:\n Vue.set(groupProfile, key, value)\n }\n }\n if (data.incomeDetailsType) {\n // someone updated their income details, create a snapshot of the haveNeeds\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n },\n 'gi.contracts/group/updateAllVotingRules': {\n validate: objectMaybeOf({\n ruleName: x => [RULE_PERCENTAGE, RULE_DISAGREEMENT].includes(x),\n ruleThreshold: number,\n expires_ms: number\n }),\n process ({ data, meta }, { state }) {\n // Update all types of proposal settings for simplicity\n if (data.ruleName && data.ruleThreshold) {\n for (const proposalSettings in state.settings.proposals) {\n Vue.set(state.settings.proposals[proposalSettings], 'rule', data.ruleName)\n Vue.set(state.settings.proposals[proposalSettings].ruleSettings[data.ruleName], 'threshold', data.ruleThreshold)\n }\n }\n\n // TODO later - support update expires_ms\n // if (data.ruleName && data.expires_ms) {\n // for (const proposalSetting in state.settings.proposals) {\n // Vue.set(state.settings.proposals[proposalSetting].ruleSettings[data.ruleName], 'expires_ms', data.expires_ms)\n // }\n // }\n }\n },\n 'gi.contracts/group/addChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n attributes: chatRoomAttributesType\n }),\n process ({ data, meta }, { state }) {\n const { name, type, privacyLevel } = data.attributes\n Vue.set(state.chatRooms, data.chatRoomID, {\n creator: meta.username,\n name,\n type,\n privacyLevel,\n deletedDate: null,\n users: []\n })\n if (!state.generalChatRoomId) {\n Vue.set(state, 'generalChatRoomId', data.chatRoomID)\n }\n }\n },\n 'gi.contracts/group/deleteChatRoom': {\n validate: (data, { getters, meta }) => {\n objectOf({ chatRoomID: string })(data)\n\n if (getters.getChatRooms[data.chatRoomID].creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state }) {\n Vue.delete(state.chatRooms, data.chatRoomID)\n }\n },\n 'gi.contracts/group/leaveChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n member: string,\n leavingGroup: boolean // if kicker is exists, it means group leaving\n }),\n process ({ data, meta }, { state }) {\n Vue.set(state.chatRooms[data.chatRoomID], 'users',\n state.chatRooms[data.chatRoomID].users.filter(u => u !== data.member))\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (meta.username === rootState.loggedIn.username && !sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n const sendingData = data.leavingGroup\n ? { member: data.member }\n : { member: data.member, username: meta.username }\n await sbp('gi.actions/chatroom/leave', { contractID: data.chatRoomID, data: sendingData })\n }\n }\n },\n 'gi.contracts/group/joinChatRoom': {\n validate: objectMaybeOf({\n username: string,\n chatRoomID: string\n }),\n process ({ data, meta }, { state }) {\n const username = data.username || meta.username\n state.chatRooms[data.chatRoomID].users.push(username)\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n const username = data.username || meta.username\n if (username === rootState.loggedIn.username) {\n if (!sbp('okTurtles.data/get', 'JOINING_GROUP') || sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n // while users are joining chatroom, they don't need to leave chatrooms\n // this is similar to setting 'JOINING_GROUP' before joining group\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', data.chatRoomID)\n await sbp('chelonia/contract/sync', data.chatRoomID)\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', undefined)\n sbp('okTurtles.data/set', 'READY_TO_JOIN_CHATROOM', false)\n }\n }\n }\n },\n 'gi.contracts/group/renameChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n name: string\n }),\n process ({ data, meta }, { state, getters }) {\n Vue.set(state.chatRooms, data.chatRoomID, {\n ...getters.getChatRooms[data.chatRoomID],\n name: data.name\n })\n }\n },\n ...((process.env.NODE_ENV === 'development' || process.env.CI) && {\n 'gi.contracts/group/forceDistributionDate': {\n validate: optional,\n process ({ meta }, { state, getters }) {\n getters.groupSettings.distributionDate = dateToPeriodStamp(meta.createdDate)\n }\n },\n 'gi.contracts/group/malformedMutation': {\n validate: objectOf({ errorType: string, sideEffect: optional(boolean) }),\n process ({ data }) {\n const ErrorType = Errors[data.errorType]\n if (data.sideEffect) return\n if (ErrorType) {\n throw new ErrorType('malformedMutation!')\n } else {\n throw new Error(`unknown error type: ${data.errorType}`)\n }\n },\n sideEffect (message, { state }) {\n if (!message.data.sideEffect) return\n sbp('gi.contracts/group/malformedMutation/process', {\n ...message,\n data: omit(message.data, ['sideEffect'])\n }, state)\n }\n }\n })\n // TODO: remove group profile when leave group is implemented\n },\n // methods are SBP selectors that are version-tracked for each contract.\n // in other words, you can use them to define SBP selectors that will\n // contain functions that you can modify across different contract versions,\n // and when the contract calls them, it will use that specific version of the\n // method.\n //\n // They are useful when used in conjunction with pushSideEffect from process\n // functions.\n //\n // IMPORTANT: they MUST begin with the name of the contract.\n methods: {\n 'gi.contracts/group/archiveProposal': async function (contractID, proposalHash, proposal) {\n const { username } = sbp('state/vuex/state').loggedIn\n const key = `proposals/${username}/${contractID}`\n const proposals = await sbp('gi.db/archive/load', key) || []\n // newest at the front of the array, oldest at the back\n proposals.unshift([proposalHash, proposal])\n while (proposals.length > MAX_ARCHIVED_PROPOSALS) {\n proposals.pop()\n }\n await sbp('gi.db/archive/save', key, proposals)\n sbp('okTurtles.events/emit', PROPOSAL_ARCHIVED, [proposalHash, proposal])\n }\n }\n})\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nexport class GIErrorIgnoreAndBan extends Error {\n // ugly boilerplate because JavaScript is stupid\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types\n constructor (...params ) {\n super(...params)\n this.name = 'GIErrorIgnoreAndBan'\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n\n// Used to throw human readable errors on UI.\nexport class GIErrorUIRuntimeError extends Error {\n constructor (...params ) {\n super(...params)\n // this.name = this.constructor.name\n this.name = 'GIErrorUIRuntimeError' // string literal so minifier doesn't overwrite\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n", "'use strict'\n\n \n \n \n \n\nexport default function mincomeProportional (haveNeeds ) {\n let totalHave = 0\n let totalNeed = 0\n const havers = []\n const needers = []\n for (const haveNeed of haveNeeds) {\n if (haveNeed.haveNeed > 0) {\n havers.push(haveNeed)\n totalHave += haveNeed.haveNeed\n } else if (haveNeed.haveNeed < 0) {\n needers.push(haveNeed)\n totalNeed += Math.abs(haveNeed.haveNeed)\n }\n }\n const totalPercent = Math.min(1, totalNeed / totalHave)\n const payments = []\n for (const haver of havers) {\n const distributionAmount = totalPercent * haver.haveNeed\n for (const needer of needers) {\n const belowPercentage = Math.abs(needer.haveNeed) / totalNeed\n payments.push({\n amount: distributionAmount * belowPercentage,\n from: haver.name,\n to: needer.name\n })\n }\n }\n return payments\n}\n", "'use strict'\n\n// greedy algorithm responsible for \"balancing\" payments\n// such that the least number of payments are made.\nexport default function minimizeTotalPaymentsCount (\n distribution \n) {\n const neederTotalReceived = {}\n const haverTotalHave = {}\n const haversSorted = []\n const needersSorted = []\n const minimizedDistribution = []\n for (const todo of distribution) {\n neederTotalReceived[todo.to] = (neederTotalReceived[todo.to] || 0) + todo.amount\n haverTotalHave[todo.from] = (haverTotalHave[todo.from] || 0) + todo.amount\n }\n for (const name in haverTotalHave) {\n haversSorted.push({ name, amount: haverTotalHave[name] })\n }\n for (const name in neederTotalReceived) {\n needersSorted.push({ name, amount: neederTotalReceived[name] })\n }\n // sort haves and needs: greatest to least\n haversSorted.sort((a, b) => b.amount - a.amount)\n needersSorted.sort((a, b) => b.amount - a.amount)\n while (haversSorted.length > 0 && needersSorted.length > 0) {\n const mostHaver = haversSorted.pop()\n const mostNeeder = needersSorted.pop()\n const diff = mostHaver.amount - mostNeeder.amount\n if (diff < 0) {\n // we used up everything the haver had\n minimizedDistribution.push({ amount: mostHaver.amount, from: mostHaver.name, to: mostNeeder.name })\n mostNeeder.amount -= mostHaver.amount\n needersSorted.push(mostNeeder)\n } else if (diff > 0) {\n // we completely filled up the needer's need and still have some left over\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n mostHaver.amount -= mostNeeder.amount\n haversSorted.push(mostHaver)\n } else {\n // a perfect match\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n }\n }\n return minimizedDistribution\n}\n", "'use strict'\n\n \n \n \n \n \n \n \n \n\n// https://github.com/okTurtles/group-income/issues/813#issuecomment-593680834\n// round all accounting to DECIMALS_MAX decimal places max to avoid consensus\n// issues that can arise due to different floating point values\n// at extreme precisions. If this becomes inadequate, instead of increasing\n// this value, switch to a different currency base, e.g. from BTC to mBTC.\nexport const DECIMALS_MAX = 8\n\nfunction commaToDots (value ) {\n // ex: \"1,55\" -> \"1.55\"\n return typeof value === 'string' ? value.replace(/,/, '.') : value.toString()\n}\n\nfunction isNumeric (nr ) {\n return !isNaN((nr ) - parseFloat(nr))\n}\n\nfunction isInDecimalsLimit (nr , decimalsMax ) {\n const decimals = nr.split('.')[1]\n return !decimals || decimals.length <= decimalsMax\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nfunction validateMincome (value , decimalsMax ) {\n const nr = commaToDots(value)\n return isNumeric(nr) && isInDecimalsLimit(nr, decimalsMax)\n}\n\nfunction decimalsOrInt (num , decimalsMax ) {\n // ex: 12.5 -> \"12.50\", but 250 -> \"250\"\n return num.toFixed(decimalsMax).replace(/\\.0+$/, '')\n}\n\nexport function saferFloat (value ) {\n // ex: 1.333333333333333333 -> 1.33333333\n return parseFloat(value.toFixed(DECIMALS_MAX))\n}\n\nexport function normalizeCurrency (value ) {\n // ex: \"1,333333333333333333\" -> 1.33333333\n return saferFloat(parseFloat(commaToDots(value)))\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nexport function mincomePositive (value ) {\n return parseFloat(commaToDots(value)) > 0\n}\n\nfunction makeCurrency (options) {\n const { symbol, symbolWithCode, decimalsMax, formatCurrency } = options\n return {\n symbol,\n symbolWithCode,\n decimalsMax,\n displayWithCurrency: (n ) => formatCurrency(decimalsOrInt(n, decimalsMax)),\n displayWithoutCurrency: (n ) => decimalsOrInt(n, decimalsMax),\n validate: (n ) => validateMincome(n, decimalsMax)\n }\n}\n\n// NOTE: if we needed for some reason, this could also be defined in\n// a json file that's read in and generates this object. For\n// example, that would allow the addition of currencies without\n// having to \"recompile\" a new version of the app.\nconst currencies = {\n USD: makeCurrency({\n symbol: '$',\n symbolWithCode: '$ USD',\n decimalsMax: 2,\n formatCurrency: amount => '$' + amount\n }),\n EUR: makeCurrency({\n symbol: '\u20AC',\n symbolWithCode: '\u20AC EUR',\n decimalsMax: 2,\n formatCurrency: amount => '\u20AC' + amount\n }),\n BTC: makeCurrency({\n symbol: '\u0243',\n symbolWithCode: '\u0243 BTC',\n decimalsMax: DECIMALS_MAX,\n formatCurrency: amount => amount + '\u0243'\n })\n}\n\nexport default currencies\n", "'use strict'\n\nimport mincomeProportional from './mincome-proportional.js'\nimport minimizeTotalPaymentsCount from './payments-minimizer.js'\nimport { cloneDeep } from '../giLodash.js'\nimport { saferFloat, DECIMALS_MAX } from '../currencies.js'\n\n \n\nconst tinyNum = 1 / Math.pow(10, DECIMALS_MAX)\n\nexport function unadjustedDistribution ({ haveNeeds = [], minimize = true } \n \n ) {\n const distribution = mincomeProportional(haveNeeds)\n return minimize ? minimizeTotalPaymentsCount(distribution) : distribution\n}\n\nexport function adjustedDistribution (\n { distribution, payments, dueOn } \n) {\n distribution = cloneDeep(distribution)\n // ensure the total is set because of how reduceDistribution works\n for (const todo of distribution) {\n todo.total = todo.amount\n }\n distribution = subtractDistributions(distribution, payments)\n // remove any todos for containing miniscule amounts\n // and pledgers who switched sides should have their todos removed\n .filter(todo => todo.amount >= tinyNum)\n for (const todo of distribution) {\n todo.amount = saferFloat(todo.amount)\n todo.total = saferFloat(todo.total)\n todo.partial = todo.total !== todo.amount\n todo.isLate = false\n todo.dueOn = dueOn\n }\n // TODO: add in latePayments to the end of the distribution\n // consider passing in latePayments\n return distribution\n}\n\n// Merges multiple payments between any combinations two of users:\nfunction reduceDistribution (payments ) {\n // Don't modify the payments list/object parameter in-place, as this is not intended:\n payments = cloneDeep(payments)\n for (let i = 0; i < payments.length; i++) {\n const paymentA = payments[i]\n for (let j = i + 1; j < payments.length; j++) {\n const paymentB = payments[j]\n\n // Were paymentA and paymentB between the same two users?\n if ((paymentA.from === paymentB.from && paymentA.to === paymentB.to) ||\n (paymentA.to === paymentB.from && paymentA.from === paymentB.to)) {\n // Add or subtract paymentB's amount to paymentA's amount, depending on the relative\n // direction of the two payments:\n paymentA.amount += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.amount\n paymentA.total += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.total\n // Remove paymentB from payments, and decrement the inner sentinal loop variable:\n payments.splice(j, 1)\n j--\n }\n }\n }\n return payments\n}\n\nfunction addDistributions (paymentsA , paymentsB ) {\n return reduceDistribution([...paymentsA, ...paymentsB])\n}\n\nfunction subtractDistributions (paymentsA , paymentsB ) {\n // Don't modify any payment list/objects parameters in-place, as this is not intended:\n paymentsB = cloneDeep(paymentsB)\n // Reverse the sign of the second operand's amounts so that the final addition is actually subtraction:\n for (const p of paymentsB) {\n p.amount *= -1\n p.total *= -1\n }\n return addDistributions(paymentsA, paymentsB)\n}\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, Errors, L } from '@common/common.js'\nimport votingRules, { ruleType, VOTE_FOR, VOTE_AGAINST, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './shared/voting/rules.js'\nimport proposals, { proposalType, proposalSettingsType, archiveProposal } from './shared/voting/proposals.js'\nimport {\n PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC, STATUS_OPEN, STATUS_CANCELLED, MAX_ARCHIVED_PROPOSALS, PROPOSAL_ARCHIVED,\n INVITE_INITIAL_CREATOR, INVITE_STATUS, PROFILE_STATUS, INVITE_EXPIRES_IN_DAYS\n} from './shared/constants.js'\nimport { paymentStatusType, paymentType, PAYMENT_COMPLETED } from './shared/payments/index.js'\nimport { merge, deepEqualJSONType, omit } from './shared/giLodash.js'\nimport { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, periodStampGivenDate, dateIsWithinPeriod, DAYS_MILLIS } from './shared/time.js'\nimport { unadjustedDistribution, adjustedDistribution } from './shared/distribution/distribution.js'\nimport currencies, { saferFloat } from './shared/currencies.js'\nimport { inviteType, chatRoomAttributesType } from './shared/types.js'\nimport { arrayOf, mapOf, objectOf, objectMaybeOf, optional, string, number, boolean, object, unionOf, tupleOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction vueFetchInitKV (obj , key , initialValue ) {\n let value = obj[key]\n if (!value) {\n Vue.set(obj, key, initialValue)\n value = obj[key]\n }\n return value\n}\n\nfunction initGroupProfile (contractID , joinedDate ) {\n return {\n globalUsername: '', // TODO: this? e.g. groupincome:greg / namecoin:bob / ens:alice\n contractID,\n joinedDate,\n nonMonetaryContributions: [],\n status: PROFILE_STATUS.ACTIVE,\n departedDate: null\n }\n}\n\nfunction initPaymentPeriod ({ getters }) {\n return {\n // this saved so that it can be used when creating a new payment\n initialCurrency: getters.groupMincomeCurrency,\n // TODO: should we also save the first period's currency exchange rate..?\n // all payments during the period use this to set their exchangeRate\n // see notes and code in groupIncomeAdjustedDistribution for details.\n // TODO: for the currency change proposal, have it update the mincomeExchangeRate\n // using .mincomeExchangeRate *= proposal.exchangeRate\n mincomeExchangeRate: 1, // modified by proposals to change mincome currency\n paymentsFrom: {}, // fromUser => toUser => Array\n // snapshot of adjusted distribution after each completed payment\n // yes, it is possible a payment began in one period and completed in another\n // in which case lastAdjustedDistribution for the previous period will be updated\n lastAdjustedDistribution: null,\n // snapshot of haveNeeds. made only when there are no payments\n haveNeedsSnapshot: null\n }\n}\n\n// NOTE: do not call any of these helper functions from within a getter b/c they modify state!\n\nfunction clearOldPayments ({ state, getters }) {\n const sortedPeriodKeys = Object.keys(state.paymentsByPeriod).sort()\n // save two periods worth of payments, max\n while (sortedPeriodKeys.length > 2) {\n const period = sortedPeriodKeys.shift()\n for (const paymentHash of getters.paymentHashesForPeriod(period)) {\n Vue.delete(state.payments, paymentHash)\n // TODO: archive the old payments in a sideEffect, not here\n }\n Vue.delete(state.paymentsByPeriod, period)\n }\n}\n\nfunction initFetchPeriodPayments ({ meta, state, getters }) {\n const period = getters.periodStampGivenDate(meta.createdDate)\n const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ getters }))\n clearOldPayments({ state, getters })\n return periodPayments\n}\n\n// this function is called each time a payment is completed or a user adjusts their income details.\n// TODO: call also when mincome is adjusted\nfunction updateCurrentDistribution ({ meta, state, getters }) {\n const curPeriodPayments = initFetchPeriodPayments({ meta, state, getters })\n const period = getters.periodStampGivenDate(meta.createdDate)\n const noPayments = Object.keys(curPeriodPayments.paymentsFrom).length === 0\n // update distributionDate if we've passed into the next period\n if (comparePeriodStamps(period, getters.groupSettings.distributionDate) > 0) {\n getters.groupSettings.distributionDate = period\n }\n // save haveNeeds if there are no payments or the haveNeeds haven't been saved yet\n if (noPayments || !curPeriodPayments.haveNeedsSnapshot) {\n curPeriodPayments.haveNeedsSnapshot = getters.haveNeedsForThisPeriod(period)\n }\n // if there are payments this period, save the adjusted distribution\n if (!noPayments) {\n updateAdjustedDistribution({ period, getters })\n }\n}\n\nfunction updateAdjustedDistribution ({ period, getters }) {\n const payments = getters.groupPeriodPayments[period]\n if (payments && payments.haveNeedsSnapshot) {\n const minimize = getters.groupSettings.minimizeDistribution\n payments.lastAdjustedDistribution = adjustedDistribution({\n distribution: unadjustedDistribution({ haveNeeds: payments.haveNeedsSnapshot, minimize }),\n payments: getters.paymentsForPeriod(period),\n dueOn: getters.dueDateForPeriod(period)\n }).filter(todo => {\n // only return todos for active members\n return getters.groupProfile(todo.to).status === PROFILE_STATUS.ACTIVE\n })\n }\n}\n\nfunction memberLeaves ({ username, dateLeft }, { meta, state, getters }) {\n state.profiles[username].status = PROFILE_STATUS.REMOVED\n state.profiles[username].departedDate = dateLeft\n // remove any todos for this member from the adjusted distribution\n updateCurrentDistribution({ meta, state, getters })\n}\n\nfunction isActionYoungerThanUser (actionMeta , userProfile ) {\n // A util function that checks if an action (or event) in a group occurred after a particular user joined a group.\n // This is used mostly for checking if a notification should be sent for that user or not.\n // e.g.) user-2 who joined a group later than user-1 (who is the creator of the group) doesn't need to receive\n // 'MEMBER_ADDED' notification for user-1.\n // In some situations, userProfile is undefined, for example, when inviteAccept is called in\n // certain situations. So we need to check for that here.\n return Boolean(userProfile) && compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/group',\n metadata: {\n validate: objectOf({\n createdDate: string,\n username: string,\n identityContractID: string\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n // TODO: We may want to get the time from the server instead of relying on\n // the client in case the client's clock isn't set correctly.\n // the only issue here is that it involves an async function...\n // See: https://github.com/okTurtles/group-income/issues/531\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n // These getters are restricted only to the contract's state.\n // Do not access state outside the contract state inside of them.\n // For example, if the getter you use tries to access `state.loggedIn`,\n // that will break the `latestContractState` function in state.js.\n // It is only safe to access state outside of the contract in a contract action's\n // `sideEffect` function (as long as it doesn't modify contract state)\n getters: {\n // we define `currentGroupState` here so that we can redefine it in state.js\n // so that we can re-use these getter definitions in state.js since they are\n // compatible with Vuex getter definitions.\n // Here `state` refers to the individual group contract's state, the equivalent\n // of `vuexRootState[someGroupContractID]`.\n currentGroupState (state) {\n return state\n },\n groupSettings (state, getters) {\n return getters.currentGroupState.settings || {}\n },\n groupProfile (state, getters) {\n return username => {\n const profiles = getters.currentGroupState.profiles\n return profiles && profiles[username]\n }\n },\n groupProfiles (state, getters) {\n const profiles = {}\n for (const username in (getters.currentGroupState.profiles || {})) {\n const profile = getters.groupProfile(username)\n if (profile.status === PROFILE_STATUS.ACTIVE) {\n profiles[username] = profile\n }\n }\n return profiles\n },\n groupMincomeAmount (state, getters) {\n return getters.groupSettings.mincomeAmount\n },\n groupMincomeCurrency (state, getters) {\n return getters.groupSettings.mincomeCurrency\n },\n periodStampGivenDate (state, getters) {\n return (recentDate ) => {\n if (typeof recentDate !== 'string') {\n recentDate = recentDate.toISOString()\n }\n const { distributionDate, distributionPeriodLength } = getters.groupSettings\n return periodStampGivenDate({\n recentDate,\n periodStart: distributionDate,\n periodLength: distributionPeriodLength\n })\n }\n },\n periodBeforePeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -len))\n }\n },\n periodAfterPeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), len))\n }\n },\n dueDateForPeriod (state, getters) {\n return (periodStamp ) => {\n return dateToPeriodStamp(\n addTimeToDate(\n dateFromPeriodStamp(getters.periodAfterPeriod(periodStamp)),\n -DAYS_MILLIS\n )\n )\n }\n },\n paymentTotalFromUserToUser (state, getters) {\n return (fromUser, toUser, periodStamp) => {\n const payments = getters.currentGroupState.payments\n const periodPayments = getters.groupPeriodPayments\n const { paymentsFrom, mincomeExchangeRate } = periodPayments[periodStamp] || {}\n // NOTE: @babel/plugin-proposal-optional-chaining would come in super-handy\n // here, but I couldn't get it to work with our linter. :(\n // https://github.com/babel/babel-eslint/issues/511\n const total = (((paymentsFrom || {})[fromUser] || {})[toUser] || []).reduce((a, hash) => {\n const payment = payments[hash]\n let { amount, exchangeRate, status } = payment.data\n if (status !== PAYMENT_COMPLETED) {\n return a\n }\n const paymentCreatedPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n // if this payment is from a previous period, then make sure to take into account\n // any proposals that passed in between the payment creation and the payment\n // completion that modified the group currency by multiplying both period's\n // exchange rates\n if (periodStamp !== paymentCreatedPeriodStamp) {\n if (paymentCreatedPeriodStamp !== getters.periodBeforePeriod(periodStamp)) {\n console.warn(`paymentTotalFromUserToUser: super old payment shouldn't exist, ignoring! (curPeriod=${periodStamp})`, JSON.stringify(payment))\n return a\n }\n exchangeRate *= periodPayments[paymentCreatedPeriodStamp].mincomeExchangeRate\n }\n return a + (amount * exchangeRate * mincomeExchangeRate)\n }, 0)\n return saferFloat(total)\n }\n },\n paymentHashesForPeriod (state, getters) {\n return (periodStamp) => {\n const periodPayments = getters.groupPeriodPayments[periodStamp]\n if (periodPayments) {\n let hashes = []\n const { paymentsFrom } = periodPayments\n for (const fromUser in paymentsFrom) {\n for (const toUser in paymentsFrom[fromUser]) {\n hashes = hashes.concat(paymentsFrom[fromUser][toUser])\n }\n }\n return hashes\n }\n }\n },\n groupMembersByUsername (state, getters) {\n return Object.keys(getters.groupProfiles)\n },\n groupMembersCount (state, getters) {\n return getters.groupMembersByUsername.length\n },\n groupMembersPending (state, getters) {\n const invites = getters.currentGroupState.invites\n const pendingMembers = {}\n for (const inviteId in invites) {\n const invite = invites[inviteId]\n if (\n invite.status === INVITE_STATUS.VALID &&\n invite.creator !== INVITE_INITIAL_CREATOR\n ) {\n pendingMembers[invites[inviteId].invitee] = {\n invitedBy: invites[inviteId].creator,\n expires: invite.expires\n }\n }\n }\n return pendingMembers\n },\n groupShouldPropose (state, getters) {\n return getters.groupMembersCount >= 3\n },\n groupProposalSettings (state, getters) {\n return (proposalType = PROPOSAL_GENERIC) => {\n return getters.groupSettings.proposals[proposalType]\n }\n },\n groupCurrency (state, getters) {\n const mincomeCurrency = getters.groupMincomeCurrency\n return mincomeCurrency && currencies[mincomeCurrency]\n },\n groupMincomeFormatted (state, getters) {\n return getters.withGroupCurrency?.(getters.groupMincomeAmount)\n },\n groupMincomeSymbolWithCode (state, getters) {\n return getters.groupCurrency?.symbolWithCode\n },\n groupPeriodPayments (state, getters) {\n // note: a lot of code expects this to return an object, so keep the || {} below\n return getters.currentGroupState.paymentsByPeriod || {}\n },\n withGroupCurrency (state, getters) {\n // TODO: If this group has no defined mincome currency, not even a default one like\n // USD, then calling this function is probably an error which should be reported.\n // Just make sure the UI doesn't break if an exception is thrown, since this is\n // bound to the UI in some location.\n return getters.groupCurrency?.displayWithCurrency\n },\n getChatRooms (state, getters) {\n return getters.currentGroupState.chatRooms\n },\n generalChatRoomId (state, getters) {\n return getters.currentGroupState.generalChatRoomId\n },\n // getter is named haveNeedsForThisPeriod instead of haveNeedsForPeriod because it uses\n // getters.groupProfiles - and that is always based on the most recent values. we still\n // pass in the current period because it's used to set the \"when\" property\n haveNeedsForThisPeriod (state, getters) {\n return (currentPeriod ) => {\n // NOTE: if we ever switch back to the \"real-time\" adjusted distribution algorithm,\n // make sure that this function also handles userExitsGroupEvent\n const groupProfiles = getters.groupProfiles // TODO: these should use the haveNeeds for the specific period's distribution period\n const haveNeeds = []\n for (const username in groupProfiles) {\n const { incomeDetailsType, joinedDate } = groupProfiles[username]\n if (incomeDetailsType) {\n const amount = groupProfiles[username][incomeDetailsType]\n const haveNeed = incomeDetailsType === 'incomeAmount' ? amount - getters.groupMincomeAmount : amount\n // construct 'when' this way in case we ever use a pro-rated algorithm\n let when = dateFromPeriodStamp(currentPeriod).toISOString()\n if (dateIsWithinPeriod({\n date: joinedDate,\n periodStart: currentPeriod,\n periodLength: getters.groupSettings.distributionPeriodLength\n })) {\n when = joinedDate\n }\n haveNeeds.push({ name: username, haveNeed, when })\n }\n }\n return haveNeeds\n }\n },\n paymentsForPeriod (state, getters) {\n return (periodStamp) => {\n const hashes = getters.paymentHashesForPeriod(periodStamp)\n const events = []\n if (hashes && hashes.length > 0) {\n const payments = getters.currentGroupState.payments\n for (const paymentHash of hashes) {\n const payment = payments[paymentHash]\n if (payment.data.status === PAYMENT_COMPLETED) {\n events.push({\n from: payment.meta.username,\n to: payment.data.toUser,\n hash: paymentHash,\n amount: payment.data.amount,\n isLate: !!payment.data.isLate,\n when: payment.data.completedDate\n })\n }\n }\n }\n return events\n }\n }\n // distributionEventsForMonth (state, getters) {\n // return (monthstamp) => {\n // // NOTE: if we ever switch back to the \"real-time\" adjusted distribution\n // // algorithm, make sure that this function also handles userExitsGroupEvent\n // const distributionEvents = getters.haveNeedEventsForMonth(monthstamp)\n // const paymentEvents = getters.paymentEventsForMonth(monthstamp)\n // distributionEvents.splice(distributionEvents.length, 0, paymentEvents)\n // return distributionEvents.sort((a, b) => compareISOTimestamps(a.data.when, b.data.when))\n // }\n // }\n },\n // NOTE: All mutations must be atomic in their edits of the contract state.\n // THEY ARE NOT to farm out any further mutations through the async actions!\n actions: {\n // this is the constructor\n 'gi.contracts/group': {\n validate: objectMaybeOf({\n invites: mapOf(string, inviteType),\n settings: objectMaybeOf({\n // TODO: add 'groupPubkey'\n groupName: string,\n groupPicture: string,\n sharedValues: string,\n mincomeAmount: number,\n mincomeCurrency: string,\n distributionDate: isPeriodStamp,\n distributionPeriodLength: number,\n minimizeDistribution: boolean,\n proposals: objectOf({\n [PROPOSAL_INVITE_MEMBER]: proposalSettingsType,\n [PROPOSAL_REMOVE_MEMBER]: proposalSettingsType,\n [PROPOSAL_GROUP_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_GENERIC]: proposalSettingsType\n })\n })\n }),\n process ({ data, meta }, { state, getters }) {\n // TODO: checkpointing: https://github.com/okTurtles/group-income/issues/354\n const initialState = merge({\n payments: {},\n paymentsByPeriod: {},\n invites: {},\n proposals: {}, // hashes => {} TODO: this, see related TODOs in GroupProposal\n settings: {\n groupCreator: meta.username,\n distributionPeriodLength: 30 * DAYS_MILLIS,\n inviteExpiryOnboarding: INVITE_EXPIRES_IN_DAYS.ON_BOARDING,\n inviteExpiryProposal: INVITE_EXPIRES_IN_DAYS.PROPOSAL\n },\n profiles: {\n [meta.username]: initGroupProfile(meta.identityContractID, meta.createdDate)\n },\n chatRooms: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n initFetchPeriodPayments({ meta, state, getters })\n }\n },\n 'gi.contracts/group/payment': {\n validate: objectMaybeOf({\n // TODO: how to handle donations to okTurtles?\n // TODO: how to handle payments to groups or users outside of this group?\n toUser: string,\n amount: number,\n currencyFromTo: tupleOf(string, string), // must be one of the keys in currencies.js (e.g. USD, EUR, etc.) TODO: handle old clients not having one of these keys, see OP_PROTOCOL_UPGRADE https://github.com/okTurtles/group-income/issues/603\n // multiply 'amount' by 'exchangeRate', which must always be\n // based on the initialCurrency of the period in which this payment was created.\n // it is then further multiplied by the period's 'mincomeExchangeRate', which\n // is modified if any proposals pass to change the mincomeCurrency\n exchangeRate: number,\n txid: string,\n status: paymentStatusType,\n paymentType: paymentType,\n details: optional(object),\n memo: optional(string)\n }),\n process ({ data, meta, hash }, { state, getters }) {\n if (data.status === PAYMENT_COMPLETED) {\n console.error(`payment: payment ${hash} cannot have status = 'completed'!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('payments cannot be instantly completed!')\n }\n Vue.set(state.payments, hash, {\n data: {\n ...data,\n groupMincome: getters.groupMincomeAmount\n },\n meta,\n history: [[meta.createdDate, hash]]\n })\n const { paymentsFrom } = initFetchPeriodPayments({ meta, state, getters })\n const fromUser = vueFetchInitKV(paymentsFrom, meta.username, {})\n const toUser = vueFetchInitKV(fromUser, data.toUser, [])\n toUser.push(hash)\n // TODO: handle completed payments here too! (for manual payment support)\n }\n },\n 'gi.contracts/group/paymentUpdate': {\n validate: objectMaybeOf({\n paymentHash: string,\n updatedProperties: objectMaybeOf({\n status: paymentStatusType,\n details: object,\n memo: string\n })\n }),\n process ({ data, meta, hash }, { state, getters }) {\n // TODO: we don't want to keep a history of all payments in memory all the time\n // https://github.com/okTurtles/group-income/issues/426\n const payment = state.payments[data.paymentHash]\n // TODO: move these types of validation errors into the validate function so\n // that they can be done before sending as well as upon receiving\n if (!payment) {\n console.error(`paymentUpdate: no payment ${data.paymentHash}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate without existing payment')\n }\n // if the payment is being modified by someone other than the person who sent or received it, throw an exception\n if (meta.username !== payment.meta.username && meta.username !== payment.data.toUser) {\n console.error(`paymentUpdate: bad username ${meta.username} != ${payment.meta.username} != ${payment.data.username}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate from bad user!')\n }\n payment.history.push([meta.createdDate, hash])\n merge(payment.data, data.updatedProperties)\n // we update \"this period\"'s snapshot 'lastAdjustedDistribution' on each completed payment\n if (data.updatedProperties.status === PAYMENT_COMPLETED) {\n payment.data.completedDate = meta.createdDate\n // update the current distribution unless this update is for a payment from the previous period\n const updatePeriodStamp = getters.periodStampGivenDate(meta.createdDate)\n const paymentPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n if (comparePeriodStamps(updatePeriodStamp, paymentPeriodStamp) > 0) {\n updateAdjustedDistribution({ period: paymentPeriodStamp, getters })\n } else {\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n }\n },\n 'gi.contracts/group/proposal': {\n validate: (data, { state, meta }) => {\n objectOf({\n proposalType: proposalType,\n proposalData: object, // data for Vue widgets\n votingRule: ruleType,\n expires_date_ms: number // calculate by grabbing proposal expiry from group properties and add to `meta.createdDate`\n })(data)\n\n const dataToCompare = omit(data.proposalData, ['reason'])\n\n // Validate this isn't a duplicate proposal\n for (const hash in state.proposals) {\n const prop = state.proposals[hash]\n if (prop.status !== STATUS_OPEN || prop.data.proposalType !== data.proposalType) {\n continue\n }\n\n if (deepEqualJSONType(omit(prop.data.proposalData, ['reason']), dataToCompare)) {\n throw new TypeError(L('There is an identical open proposal.'))\n }\n\n // TODO - verify if type of proposal already exists (SETTING_CHANGE).\n }\n },\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.proposals, hash, {\n data,\n meta,\n votes: { [meta.username]: VOTE_FOR },\n status: STATUS_OPEN,\n payload: null // set later by group/proposalVote\n })\n // TODO: save all proposals disk so that we only keep open proposals in memory\n // TODO: create a global timer to auto-pass/archive expired votes\n // make sure to set that proposal's status as STATUS_EXPIRED if it's expired\n },\n sideEffect ({ contractID, meta, data }, { getters }) {\n const { loggedIn } = sbp('state/vuex/state')\n const typeToSubTypeMap = {\n [PROPOSAL_INVITE_MEMBER]: 'ADD_MEMBER',\n [PROPOSAL_REMOVE_MEMBER]: 'REMOVE_MEMBER',\n [PROPOSAL_GROUP_SETTING_CHANGE]: 'CHANGE_MINCOME',\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: 'CHANGE_VOTING_RULE',\n [PROPOSAL_GENERIC]: 'GENERIC'\n }\n\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'NEW_PROPOSAL', {\n groupID: contractID,\n creator: meta.username,\n subtype: typeToSubTypeMap[data.proposalType]\n })\n }\n }\n },\n 'gi.contracts/group/proposalVote': {\n validate: objectOf({\n proposalHash: string,\n vote: string,\n passPayload: optional(unionOf(object, string)) // TODO: this, somehow we need to send an OP_KEY_ADD GIMessage to add a generated once-only writeonly message public key to the contract, and (encrypted) include the corresponding invite link, also, we need all clients to verify that this message/operation was valid to prevent a hacked client from adding arbitrary OP_KEY_ADD messages, and automatically ban anyone generating such messages\n }),\n process (message, { state }) {\n const { data, hash, meta } = message\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalVote: no proposal for ${data.proposalHash}!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n }\n Vue.set(proposal.votes, meta.username, data.vote)\n // TODO: handle vote pass/fail\n // check if proposal is expired, if so, ignore (but log vote)\n if (new Date(meta.createdDate).getTime() > proposal.data.expires_date_ms) {\n console.warn('proposalVote: vote on expired proposal!', { proposal, data, meta })\n // TODO: display warning or something\n return\n }\n // see if this is a deciding vote\n const result = votingRules[proposal.data.votingRule](state, proposal.data.proposalType, proposal.votes)\n if (result === VOTE_FOR || result === VOTE_AGAINST) {\n // handles proposal pass or fail, will update proposal.status accordingly\n proposals[proposal.data.proposalType][result](state, message)\n Vue.set(proposal, 'dateClosed', meta.createdDate)\n }\n },\n sideEffect ({ contractID, data, meta }, { state, getters }) {\n const proposal = state.proposals[data.proposalHash]\n const { loggedIn } = sbp('state/vuex/state')\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (proposal?.dateClosed &&\n isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'PROPOSAL_CLOSED', {\n groupID: contractID,\n creator: meta.username,\n proposalStatus: proposal.status\n })\n }\n }\n },\n 'gi.contracts/group/proposalCancel': {\n validate: objectOf({\n proposalHash: string\n }),\n process ({ data, meta, contractID }, { state }) {\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalCancel: no proposal for ${data.proposalHash}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n } else if (proposal.meta.username !== meta.username) {\n console.error(`proposalCancel: proposal ${data.proposalHash} belongs to ${proposal.meta.username} not ${meta.username}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalWithdraw for wrong user!')\n }\n Vue.set(proposal, 'status', STATUS_CANCELLED)\n archiveProposal({ state, proposalHash: data.proposalHash, proposal, contractID })\n }\n },\n 'gi.contracts/group/removeMember': {\n validate: (data, { state, getters, meta }) => {\n objectOf({\n member: string, // username to remove\n reason: optional(string),\n automated: optional(boolean),\n // In case it happens in a big group (by proposal)\n // we need to validate the associated proposal.\n proposalHash: optional(string),\n proposalPayload: optional(objectOf({\n secret: string // NOTE: simulate the OP_KEY_* stuff for now\n }))\n })(data)\n\n const memberToRemove = data.member\n const membersCount = getters.groupMembersCount\n\n if (!state.profiles[memberToRemove]) {\n throw new TypeError(L('Not part of the group.'))\n }\n if (membersCount === 1 || memberToRemove === meta.username) {\n throw new TypeError(L('Cannot remove yourself.'))\n }\n\n if (membersCount < 3) {\n // In a small group only the creator can remove someone\n // TODO: check whether meta.username has required admin permissions\n if (meta.username !== state.settings.groupCreator) {\n throw new TypeError(L('Only the group creator can remove members.'))\n }\n } else {\n // In a big group a removal can only happen through a proposal\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // TODO this\n throw new TypeError(L('Admin credentials needed and not implemented yet.'))\n }\n\n if (!proposal.payload || proposal.payload.secret !== data.proposalPayload.secret) {\n throw new TypeError(L('Invalid associated proposal.'))\n }\n }\n },\n process ({ data, meta }, { state, getters }) {\n memberLeaves(\n { username: data.member, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n },\n sideEffect ({ data, meta, contractID }, { state, getters }) {\n const rootState = sbp('state/vuex/state')\n const contracts = rootState.contracts || {}\n const { username } = rootState.loggedIn\n\n if (data.member === username) {\n // If this member is re-joining the group, ignore the rest\n // so the member doesn't remove themself again.\n if (sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n return\n }\n\n const groupIdToSwitch = Object.keys(contracts)\n .find(cID => contracts[cID].type === 'gi.contracts/group' &&\n cID !== contractID && rootState[cID].settings) || null\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {})\n sbp('state/vuex/commit', 'setCurrentGroupId', groupIdToSwitch)\n // we can't await on this in here, because it will cause a deadlock, since Chelonia processes\n // this sideEffect on the eventqueue for this contractID, and /remove uses that same eventqueue\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown by /remove ${contractID}:`, e)\n })\n // this looks crazy, but doing this was necessary to fix a race condition in the\n // group-member-removal Cypress tests where due to the ordering of asynchronous events\n // we were getting the same latestHash upon re-logging in for test \"user2 rejoins groupA\".\n // We add it to the same queue as '/remove' above gets run on so that it is run after\n // contractID is removed. See also comments in 'gi.actions/identity/login'.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/saveOurLoginState'])\n .then(function () {\n const router = sbp('controller/router')\n const switchFrom = router.currentRoute.path\n const switchTo = groupIdToSwitch ? '/dashboard' : '/'\n if (switchFrom !== '/join' && switchFrom !== switchTo) {\n router.push({ path: switchTo }).catch(console.warn)\n }\n }).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown during queueEvent to ${contractID} by saveOurLoginState:`, e)\n })\n // TODO - #828 remove other group members contracts if applicable\n } else {\n const myProfile = getters.groupProfile(username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n const memberRemovedThemselves = data.member === meta.username\n\n sbp('gi.notifications/emit', // emit a notification for a member removal.\n memberRemovedThemselves ? 'MEMBER_LEFT' : 'MEMBER_REMOVED',\n {\n groupID: contractID,\n username: memberRemovedThemselves ? meta.username : data.member\n })\n }\n // TODO - #828 remove the member contract if applicable.\n // problem is, if they're in another group we're also a part of, or if we\n // have a DM with them, we don't want to do this. may need to use manual reference counting\n // sbp('chelonia/contract/release', getters.groupProfile(data.member).contractID)\n }\n // TODO - #850 verify open proposals and see if they need some re-adjustment.\n }\n },\n 'gi.contracts/group/removeOurselves': {\n validate: objectMaybeOf({\n reason: string\n }),\n process ({ data, meta, contractID }, { state, getters }) {\n memberLeaves(\n { username: meta.username, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', {\n meta,\n data: { member: meta.username, reason: data.reason || '' },\n contractID\n }]\n )\n }\n },\n 'gi.contracts/group/invite': {\n validate: inviteType,\n process ({ data, meta }, { state }) {\n Vue.set(state.invites, data.inviteSecret, data)\n }\n },\n 'gi.contracts/group/inviteAccept': {\n validate: objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n }),\n process ({ data, meta }, { state }) {\n console.debug('inviteAccept:', data, state.invites)\n const invite = state.invites[data.inviteSecret]\n if (invite.status !== INVITE_STATUS.VALID) {\n console.error(`inviteAccept: invite for ${meta.username} is: ${invite.status}`)\n return\n }\n Vue.set(invite.responses, meta.username, true)\n if (Object.keys(invite.responses).length === invite.quantity) {\n invite.status = INVITE_STATUS.USED\n }\n // TODO: ensure `meta.username` is unique for the lifetime of the username\n // since we are making it possible for the same username to leave and\n // rejoin the group. All of their past posts will be re-associated with\n // them upon re-joining.\n Vue.set(state.profiles, meta.username, initGroupProfile(meta.identityContractID, meta.createdDate))\n // If we're triggered by handleEvent in state.js (and not latestContractState)\n // then the asynchronous sideEffect function will get called next\n // and we will subscribe to this new user's identity contract\n },\n // !! IMPORANT!!\n // Actions here MUST NOT modify contract state!\n // They MUST NOT call 'commit'!\n // They should only coordinate the actions of outside contracts.\n // Otherwise `latestContractState` and `handleEvent` will not produce same state!\n async sideEffect ({ meta, contractID }, { state }) {\n const { loggedIn } = sbp('state/vuex/state')\n const { profiles = {} } = state\n\n // TODO: per #257 this will ,have to be encompassed in a recoverable transaction\n // however per #610 that might be handled in handleEvent (?), or per #356 might not be needed\n if (meta.username === loggedIn.username) {\n // we're the person who just accepted the group invite\n // so subscribe to founder's IdentityContract & everyone else's\n for (const name in profiles) {\n if (name !== loggedIn.username) {\n await sbp('chelonia/contract/sync', profiles[name].contractID)\n }\n }\n } else {\n const myProfile = profiles[loggedIn.username]\n // we're an existing member of the group getting notified that a\n // new member has joined, so subscribe to their identity contract\n await sbp('chelonia/contract/sync', meta.identityContractID)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'MEMBER_ADDED', { // emit a notification for a member addition.\n groupID: contractID,\n username: meta.username\n })\n }\n }\n }\n },\n 'gi.contracts/group/inviteRevoke': {\n validate: (data, { state, meta }) => {\n objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n })(data)\n\n if (!state.invites[data.inviteSecret]) {\n throw new TypeError(L('The link does not exist.'))\n }\n },\n process ({ data, meta }, { state }) {\n const invite = state.invites[data.inviteSecret]\n Vue.set(invite, 'status', INVITE_STATUS.REVOKED)\n }\n },\n 'gi.contracts/group/updateSettings': {\n // OPTIMIZE: Make this custom validation function\n // reusable accross other future validators\n validate: objectMaybeOf({\n groupName: x => typeof x === 'string',\n groupPicture: x => typeof x === 'string',\n sharedValues: x => typeof x === 'string',\n mincomeAmount: x => typeof x === 'number' && x > 0,\n mincomeCurrency: x => typeof x === 'string'\n }),\n process ({ meta, data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/group/groupProfileUpdate': {\n validate: objectMaybeOf({\n incomeDetailsType: x => ['incomeAmount', 'pledgeAmount'].includes(x),\n incomeAmount: x => typeof x === 'number' && x >= 0,\n pledgeAmount: x => typeof x === 'number' && x >= 0,\n nonMonetaryAdd: string,\n nonMonetaryEdit: objectOf({\n replace: string,\n with: string\n }),\n nonMonetaryRemove: string,\n paymentMethods: arrayOf(\n objectOf({\n name: string,\n value: string\n })\n )\n }),\n process ({ data, meta }, { state, getters }) {\n const groupProfile = state.profiles[meta.username]\n const nonMonetary = groupProfile.nonMonetaryContributions\n for (const key in data) {\n const value = data[key]\n switch (key) {\n case 'nonMonetaryAdd':\n nonMonetary.push(value)\n break\n case 'nonMonetaryRemove':\n nonMonetary.splice(nonMonetary.indexOf(value), 1)\n break\n case 'nonMonetaryEdit':\n nonMonetary.splice(nonMonetary.indexOf(value.replace), 1, value.with)\n break\n default:\n Vue.set(groupProfile, key, value)\n }\n }\n if (data.incomeDetailsType) {\n // someone updated their income details, create a snapshot of the haveNeeds\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n },\n 'gi.contracts/group/updateAllVotingRules': {\n validate: objectMaybeOf({\n ruleName: x => [RULE_PERCENTAGE, RULE_DISAGREEMENT].includes(x),\n ruleThreshold: number,\n expires_ms: number\n }),\n process ({ data, meta }, { state }) {\n // Update all types of proposal settings for simplicity\n if (data.ruleName && data.ruleThreshold) {\n for (const proposalSettings in state.settings.proposals) {\n Vue.set(state.settings.proposals[proposalSettings], 'rule', data.ruleName)\n Vue.set(state.settings.proposals[proposalSettings].ruleSettings[data.ruleName], 'threshold', data.ruleThreshold)\n }\n }\n\n // TODO later - support update expires_ms\n // if (data.ruleName && data.expires_ms) {\n // for (const proposalSetting in state.settings.proposals) {\n // Vue.set(state.settings.proposals[proposalSetting].ruleSettings[data.ruleName], 'expires_ms', data.expires_ms)\n // }\n // }\n }\n },\n 'gi.contracts/group/addChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n attributes: chatRoomAttributesType\n }),\n process ({ data, meta }, { state }) {\n const { name, type, privacyLevel } = data.attributes\n Vue.set(state.chatRooms, data.chatRoomID, {\n creator: meta.username,\n name,\n type,\n privacyLevel,\n deletedDate: null,\n users: []\n })\n if (!state.generalChatRoomId) {\n Vue.set(state, 'generalChatRoomId', data.chatRoomID)\n }\n }\n },\n 'gi.contracts/group/deleteChatRoom': {\n validate: (data, { getters, meta }) => {\n objectOf({ chatRoomID: string })(data)\n\n if (getters.getChatRooms[data.chatRoomID].creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state }) {\n Vue.delete(state.chatRooms, data.chatRoomID)\n }\n },\n 'gi.contracts/group/leaveChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n member: string,\n leavingGroup: boolean // if kicker is exists, it means group leaving\n }),\n process ({ data, meta }, { state }) {\n Vue.set(state.chatRooms[data.chatRoomID], 'users',\n state.chatRooms[data.chatRoomID].users.filter(u => u !== data.member))\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (meta.username === rootState.loggedIn.username && !sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n const sendingData = data.leavingGroup\n ? { member: data.member }\n : { member: data.member, username: meta.username }\n await sbp('gi.actions/chatroom/leave', { contractID: data.chatRoomID, data: sendingData })\n }\n }\n },\n 'gi.contracts/group/joinChatRoom': {\n validate: objectMaybeOf({\n username: string,\n chatRoomID: string\n }),\n process ({ data, meta }, { state }) {\n const username = data.username || meta.username\n state.chatRooms[data.chatRoomID].users.push(username)\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n const username = data.username || meta.username\n if (username === rootState.loggedIn.username) {\n if (!sbp('okTurtles.data/get', 'JOINING_GROUP') || sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n // while users are joining chatroom, they don't need to leave chatrooms\n // this is similar to setting 'JOINING_GROUP' before joining group\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', data.chatRoomID)\n await sbp('chelonia/contract/sync', data.chatRoomID)\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', undefined)\n sbp('okTurtles.data/set', 'READY_TO_JOIN_CHATROOM', false)\n }\n }\n }\n },\n 'gi.contracts/group/renameChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n name: string\n }),\n process ({ data, meta }, { state, getters }) {\n Vue.set(state.chatRooms, data.chatRoomID, {\n ...getters.getChatRooms[data.chatRoomID],\n name: data.name\n })\n }\n },\n ...((process.env.NODE_ENV === 'development' || process.env.CI) && {\n 'gi.contracts/group/forceDistributionDate': {\n validate: optional,\n process ({ meta }, { state, getters }) {\n getters.groupSettings.distributionDate = dateToPeriodStamp(meta.createdDate)\n }\n },\n 'gi.contracts/group/malformedMutation': {\n validate: objectOf({ errorType: string, sideEffect: optional(boolean) }),\n process ({ data }) {\n const ErrorType = Errors[data.errorType]\n if (data.sideEffect) return\n if (ErrorType) {\n throw new ErrorType('malformedMutation!')\n } else {\n throw new Error(`unknown error type: ${data.errorType}`)\n }\n },\n sideEffect (message, { state }) {\n if (!message.data.sideEffect) return\n sbp('gi.contracts/group/malformedMutation/process', {\n ...message,\n data: omit(message.data, ['sideEffect'])\n }, state)\n }\n }\n })\n // TODO: remove group profile when leave group is implemented\n },\n // methods are SBP selectors that are version-tracked for each contract.\n // in other words, you can use them to define SBP selectors that will\n // contain functions that you can modify across different contract versions,\n // and when the contract calls them, it will use that specific version of the\n // method.\n //\n // They are useful when used in conjunction with pushSideEffect from process\n // functions.\n //\n // IMPORTANT: they MUST begin with the name of the contract.\n methods: {\n 'gi.contracts/group/archiveProposal': async function (contractID, proposalHash, proposal) {\n const { username } = sbp('state/vuex/state').loggedIn\n const key = `proposals/${username}/${contractID}`\n const proposals = await sbp('gi.db/archive/load', key) || []\n // newest at the front of the array, oldest at the back\n proposals.unshift([proposalHash, proposal])\n while (proposals.length > MAX_ARCHIVED_PROPOSALS) {\n proposals.pop()\n }\n await sbp('gi.db/archive/save', key, proposals)\n sbp('okTurtles.events/emit', PROPOSAL_ARCHIVED, [proposalHash, proposal])\n }\n }\n})\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/identity.js b/test/contracts/identity.js index f2213bfc0b..406d8042aa 100644 --- a/test/contracts/identity.js +++ b/test/contracts/identity.js @@ -345,14 +345,14 @@ var objectOf = (typeObj, _scope = "Object") => { } const undefAttr = typeAttrs.find((property) => { const propertyTypeFn = typeObj[property]; - return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); }); if (undefAttr) { throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); } const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { const typeFn = typeObj[key]; - if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { return Object.assign(acc, {}); } else { return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); @@ -361,7 +361,10 @@ var objectOf = (typeObj, _scope = "Object") => { return typeAttrs.reduce(reducer, {}); } object2.type = () => { - const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); return `{| ${props.join(",\n ")} |}`; diff --git a/test/contracts/identity.js.map b/test/contracts/identity.js.map index 55d720bdeb..f0dec6c3c3 100644 --- a/test/contracts/identity.js.map +++ b/test/contracts/identity.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/validators.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/identity.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "// Matches strings of plain ASCII letters and digits, hyphens and underscores.\nexport const allowedUsernameCharacters = (value ) => /^[\\w-]*$/.test(value)\n\n// Matches non-empty strings of plain ASCII letters and digits.\nexport const alphanumeric = (value ) => /^[A-Za-z\\d]+$/.test(value)\n\nexport const noConsecutiveHyphensOrUnderscores = (value ) => !value.includes('--') && !value.includes('__')\n\nexport const noLeadingOrTrailingHyphen = (value ) => !value.startsWith('-') && !value.endsWith('-')\nexport const noLeadingOrTrailingUnderscore = (value ) => !value.startsWith('_') && !value.endsWith('_')\n\nexport const decimals = (digits ) => (value ) => Number.isInteger(value * Math.pow(10, digits))\n\nexport const noUppercase = (value ) => value.toLowerCase() === value\n\nexport const noWhitespace = (value ) => /^\\S+$/.test(value)\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\n\nimport { Vue, L } from '@common/common.js'\nimport { merge } from './shared/giLodash.js'\nimport { objectOf, objectMaybeOf, arrayOf, string, object } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n allowedUsernameCharacters,\n noConsecutiveHyphensOrUnderscores,\n noLeadingOrTrailingHyphen,\n noLeadingOrTrailingUnderscore,\n noUppercase\n} from './shared/validators.js'\n\nimport { IDENTITY_USERNAME_MAX_CHARS } from './shared/constants.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/identity',\n getters: {\n currentIdentityState (state) {\n return state\n },\n loginState (state, getters) {\n return getters.currentIdentityState.loginState\n }\n },\n actions: {\n 'gi.contracts/identity': {\n validate: (data, { state, meta }) => {\n objectMaybeOf({\n attributes: objectMaybeOf({\n username: string,\n email: string,\n picture: string\n })\n })(data)\n const { username } = data.attributes\n if (username.length > IDENTITY_USERNAME_MAX_CHARS) {\n throw new TypeError(`A username cannot exceed ${IDENTITY_USERNAME_MAX_CHARS} characters.`)\n }\n if (!allowedUsernameCharacters(username)) {\n throw new TypeError('A username cannot contain disallowed characters.')\n }\n if (!noConsecutiveHyphensOrUnderscores(username)) {\n throw new TypeError('A username cannot contain two consecutive hyphens or underscores.')\n }\n if (!noLeadingOrTrailingHyphen(username)) {\n throw new TypeError('A username cannot start or end with a hyphen.')\n }\n if (!noLeadingOrTrailingUnderscore(username)) {\n throw new TypeError('A username cannot start or end with an underscore.')\n }\n if (!noUppercase(username)) {\n throw new TypeError('A username cannot contain uppercase letters.')\n }\n },\n process ({ data }, { state }) {\n const initialState = merge({\n settings: {},\n attributes: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/identity/setAttributes': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.attributes, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/deleteAttributes': {\n validate: arrayOf(string),\n process ({ data }, { state }) {\n for (const attribute of data) {\n Vue.delete(state.attributes, attribute)\n }\n }\n },\n 'gi.contracts/identity/updateSettings': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/setLoginState': {\n validate: objectOf({\n groupIds: arrayOf(string)\n }),\n process ({ data }, { state }) {\n Vue.set(state, 'loginState', data)\n },\n sideEffect ({ contractID }) {\n // it only makes sense to call updateLoginStateUponLogin for ourselves\n if (contractID === sbp('state/vuex/getters').ourIdentityContractId) {\n // makes sure that updateLoginStateUponLogin gets run after the entire identity\n // state has been synced, this way we don't end up joining groups we've left, etc.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/updateLoginStateUponLogin'])\n .catch((e) => {\n sbp('gi.notifications/emit', 'ERROR', {\n message: L(\"Failed to join groups we're part of on another device. Not catastrophic, but could lead to problems. {errName}: '{errMsg}'\", {\n errName: e.name,\n errMsg: e.message || '?'\n })\n })\n })\n }\n }\n }\n }\n})\n"], - "mappings": ";;;AAKA,IAAM,YAAY,CAAC;AACnB,IAAM,UAAU,CAAC;AACjB,IAAM,gBAAgB,CAAC;AACvB,IAAM,gBAAgB,CAAC;AACvB,IAAM,kBAAkB,CAAC;AACzB,IAAM,kBAAkB,CAAC;AAEzB,IAAM,eAAe;AAErB,aAAc,aAAa,MAAM;AAC/B,QAAM,SAAS,mBAAmB,QAAQ;AAC1C,MAAI,CAAC,UAAU,WAAW;AACxB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AAGA,aAAW,WAAW,CAAC,gBAAgB,WAAW,cAAc,SAAS,aAAa,GAAG;AACvF,QAAI,SAAS;AACX,iBAAW,UAAU,SAAS;AAC5B,YAAI,OAAO,QAAQ,UAAU,IAAI,MAAM;AAAO;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACA,SAAO,UAAU,UAAU,KAAK,QAAQ,QAAQ,OAAO,GAAG,IAAI;AAChE;AAEO,4BAA6B,UAAU;AAC5C,QAAM,eAAe,aAAa,KAAK,QAAQ;AAC/C,MAAI,iBAAiB,MAAM;AACzB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AACA,SAAO,aAAa;AACtB;AAEA,IAAM,qBAAqB;AAAA,EACzB,0BAA0B,SAAU,MAAM;AACxC,UAAM,aAAa,CAAC;AACpB,eAAW,YAAY,MAAM;AAC3B,YAAM,SAAS,mBAAmB,QAAQ;AAC1C,UAAI,UAAU,WAAW;AACvB,QAAC,SAAQ,QAAQ,QAAQ,KAAK,6DAA6D,WAAW;AAAA,MACxG,WAAW,OAAO,KAAK,cAAc,YAAY;AAC/C,YAAI,gBAAgB,WAAW;AAE7B,UAAC,SAAQ,QAAQ,QAAQ,KAAK,6CAA6C,gDAAgD;AAAA,QAC7H;AACA,cAAM,KAAK,UAAU,YAAY,KAAK;AACtC,mBAAW,KAAK,QAAQ;AAExB,YAAI,CAAC,QAAQ,SAAS;AACpB,kBAAQ,UAAU,EAAE,OAAO,CAAC,EAAE;AAAA,QAChC;AAEA,YAAI,aAAa,GAAG,gBAAgB;AAClC,aAAG,KAAK,QAAQ,QAAQ,KAAK;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,4BAA4B,SAAU,MAAM;AAC1C,eAAW,YAAY,MAAM;AAC3B,UAAI,CAAC,gBAAgB,WAAW;AAC9B,cAAM,IAAI,MAAM,0CAA0C,UAAU;AAAA,MACtE;AACA,aAAO,UAAU;AAAA,IACnB;AAAA,EACF;AAAA,EACA,2BAA2B,SAAU,MAAM;AACzC,QAAI,4BAA4B,OAAO,KAAK,IAAI,CAAC;AACjD,WAAO,IAAI,0BAA0B,IAAI;AAAA,EAC3C;AAAA,EACA,wBAAwB,SAAU,MAAM;AACtC,eAAW,YAAY,MAAM;AAC3B,UAAI,UAAU,WAAW;AACvB,cAAM,IAAI,MAAM,mDAAmD;AAAA,MACrE;AACA,sBAAgB,YAAY;AAAA,IAC9B;AAAA,EACF;AAAA,EACA,sBAAsB,SAAU,MAAM;AACpC,eAAW,YAAY,MAAM;AAC3B,aAAO,gBAAgB;AAAA,IACzB;AAAA,EACF;AAAA,EACA,oBAAoB,SAAU,KAAK;AACjC,WAAO,UAAU;AAAA,EACnB;AAAA,EACA,0BAA0B,SAAU,QAAQ;AAC1C,kBAAc,KAAK,MAAM;AAAA,EAC3B;AAAA,EACA,0BAA0B,SAAU,QAAQ,QAAQ;AAClD,QAAI,CAAC,cAAc;AAAS,oBAAc,UAAU,CAAC;AACrD,kBAAc,QAAQ,KAAK,MAAM;AAAA,EACnC;AAAA,EACA,4BAA4B,SAAU,UAAU,QAAQ;AACtD,QAAI,CAAC,gBAAgB;AAAW,sBAAgB,YAAY,CAAC;AAC7D,oBAAgB,UAAU,KAAK,MAAM;AAAA,EACvC;AACF;AAEA,mBAAmB,0BAA0B,kBAAkB;AAE/D,IAAO,iBAAQ;;;ACpFf;;;ACNA;AACA;;;ACwBO,mBAAoB,KAAK;AAC9B,SAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;AACvC;AAEA,2BAA4B,KAAK;AAC/B,QAAM,gBAAgB,OAAO,OAAO,QAAQ;AAE5C,SAAO,iBAAiB,OAAO,UAAU,SAAS,KAAK,GAAG,MAAM,qBAAqB,OAAO,UAAU,SAAS,KAAK,GAAG,MAAM;AAC/H;AAEO,eAAgB,KAAK,KAAK;AAC/B,aAAW,OAAO,KAAK;AACrB,UAAM,QAAQ,kBAAkB,IAAI,IAAI,IAAI,UAAU,IAAI,IAAI,IAAI;AAClE,QAAI,SAAS,kBAAkB,IAAI,IAAI,GAAG;AACxC,YAAM,IAAI,MAAM,KAAK;AACrB;AAAA,IACF;AACA,QAAI,OAAO,SAAS,IAAI;AAAA,EAC1B;AACA,SAAO;AACT;;;ADxCO,IAAM,gBAAgB;AAAA,EAC3B,cAAc,CAAC,OAAO;AAAA,EACtB,cAAc,CAAC,KAAK,MAAM,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EAEtF,qBAAqB;AACvB;AAEA,IAAM,YAAY,CAAC,IAAI,YAAY;AACjC,MAAI,QAAQ,aAAa,QAAQ,OAAO;AACtC,QAAI,SAAS;AACb,QAAI,QAAQ,QAAQ,KAAK;AACvB,eAAS,UAAU,MAAM;AACzB,aAAO,aAAa,KAAK,QAAQ,QAAQ;AACzC,aAAO,aAAa,KAAK,GAAG;AAAA,IAC9B;AACA,OAAG,cAAc;AACjB,OAAG,YAAY,UAAU,SAAS,QAAQ,OAAO,MAAM,CAAC;AAAA,EAC1D;AACF;AAWA,IAAI,UAAU,aAAa;AAAA,EACzB,MAAM;AAAA,EACN,QAAQ;AACV,CAAC;;;AElDD;AACA;;;ACNA,IAAM,QAAQ;AAEC,kBAAmB,YAAmB,MAAqB;AACxE,QAAM,WAAW,KAAK;AAGtB,QAAM,oBACH,OAAO,aAAa,YAAY,aAAa,OAC1C,WACA;AAGN,SAAO,QAAO,QAAQ,OAAO,oBAAqB,OAAO,SAAS,OAAO;AAEvE,QAAI,QAAO,QAAQ,OAAO,OAAO,QAAO,QAAQ,MAAM,YAAY,KAAK;AACrE,aAAO;AAAA,IACT;AAEA,UAAM,mBAGJ,OAAO,UAAU,eAAe,KAAK,mBAAmB,OAAO,IAC3D,kBAAkB,WAClB;AAGN,QAAI,qBAAqB,QAAQ,qBAAqB,QAAW;AAC/D,aAAO;AAAA,IACT;AACA,WAAO,OAAO,gBAAgB;AAAA,EAChC,CAAC;AACH;;;ADtBA,KAAI,UAAU,IAAI;AAClB,KAAI,UAAU,QAAQ;AAEtB,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,0BAAgD,CAAC;AAOvD,IAAM,kBAAkB;AAAA,EACtB,GAAG;AAAA,EACH,cAAc,CAAC,SAAS,QAAQ,OAAO,QAAQ;AAAA,EAC/C,cAAc,CAAC,KAAK,KAAK,MAAM,UAAU,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EACrG,qBAAqB;AACvB;AAEA,IAAI,kBAAkB;AACtB,IAAI,sBAAsB,gBAAgB,MAAM,GAAG,EAAE;AACrD,IAAI,0BAA0B;AAY9B,eAAI,0BAA0B;AAAA,EAC5B,qBAAqB,oBAAqB,UAAiC;AAEzE,UAAM,CAAC,gBAAgB,SAAS,YAAY,EAAE,MAAM,GAAG;AAGvD,QAAI,SAAS,YAAY,MAAM,gBAAgB,YAAY;AAAG;AAI9D,QAAI,iBAAiB;AAAqB;AAG1C,QAAI,iBAAiB,qBAAqB;AACxC,wBAAkB;AAClB,4BAAsB;AACtB,gCAA0B;AAC1B;AAAA,IACF;AACA,QAAI;AACF,gCAA2B,MAAM,eAAI,4BAA4B,QAAQ,KAAM;AAG/E,wBAAkB;AAClB,4BAAsB;AAAA,IACxB,SAAS,OAAP;AACA,cAAQ,MAAM,KAAK;AAAA,IACrB;AAAA,EACF;AACF,CAAC;AA0CM,kBAAmB,MAAiC;AACzD,QAAM,IAAI;AAAA,IACR,OAAO;AAAA,EACT;AACA,aAAW,OAAO,MAAM;AACtB,MAAE,GAAG,UAAU,IAAI;AACnB,MAAE,IAAI,SAAS,KAAK;AAAA,EACtB;AACA,SAAO;AACT;AAEe,WACb,KACA,MACQ;AACR,SAAO,SAAS,wBAAwB,QAAQ,KAAK,IAAI,EAEtD,QAAQ,iBAAiB,QAAQ;AACtC;AAgBA,kBAAmB,aAAa;AAC9B,SAAO,WAAU,SAAS,aAAa,eAAe;AACxD;AAEA,KAAI,UAAU,QAAQ;AAAA,EACpB,YAAY;AAAA,EACZ,OAAO;AAAA,IACL,MAAM,CAAC,QAAQ,KAAK;AAAA,IACpB,KAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA,SAAS;AAAA,EACX;AAAA,EACA,QAAQ,SAAU,GAAG,SAAS;AAC5B,UAAM,OAAO,QAAQ,SAAS,GAAG;AACjC,UAAM,cAAc,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,CAAC;AACpD,QAAI,CAAC,aAAa;AAChB,cAAQ,KAAK,yDAAyD,IAAI;AAC1E,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,IAAI;AAAA,IAChD;AAEA,QAAI,QAAQ,MAAM,QAAQ,OAAO,QAAQ,KAAK,MAAM,WAAW,UAAU;AACvE,cAAQ,KAAK,MAAM,MAAM;AAAA,IAC3B;AACA,QAAI,QAAQ,MAAM,SAAS;AACzB,YAAM,SAAS,KAAI,QAAQ,WAAW,SAAS,WAAW,IAAI,SAAS;AAEvE,aAAO,OAAO,OAAO,KAAK;AAAA,QACxB,IAAI,CAAC,QAAQ,SAAS;AACpB,cAAI,QAAQ,QAAQ;AAClB,mBAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,GAAG,IAAI;AAAA,UACnD,OAAO;AACL,mBAAO,EAAE,KAAK,GAAG,IAAI;AAAA,UACvB;AAAA,QACF;AAAA,QACA,IAAI,OAAK;AAAA,MACX,CAAC;AAAA,IACH,OAAO;AACL,UAAI,CAAC,QAAQ,KAAK;AAAU,gBAAQ,KAAK,WAAW,CAAC;AACrD,cAAQ,KAAK,SAAS,YAAY,SAAS,WAAW;AACtD,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,IAAI;AAAA,IAC1C;AAAA,EACF;AACF,CAAC;;;AE5IM,IAAM,cAAc,OAAO,SAAS;AACpC,IAAM,UAAU,OAAK,MAAM;AAC3B,IAAM,QAAQ,OAAK,MAAM;AACzB,IAAM,UAAU,OAAK,OAAO,MAAM;AAGlC,IAAM,WAAW,OAAK,OAAO,MAAM;AACnC,IAAM,WAAW,OAAK,CAAC,MAAM,CAAC,KAAK,OAAO,MAAM;AAChD,IAAM,aAAa,OAAK,OAAO,MAAM;AAcrC,IAAM,UAAU,CAAC,QAAQ,aAAa;AAC3C,MAAI,WAAW,OAAO,IAAI;AAAG,WAAO,OAAO,KAAK,QAAQ;AACxD,SAAO,OAAO,QAAQ;AACxB;AAGO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YACE,SACA,cACA,WACA,OACA,WAAmB,IACnB,YAAqB,IACrB;AACA,UAAM,aAAa,WACjB,YAAY,0BAA0B,YAAY;AACpD,UAAM,UAAU;AAChB,SAAK,eAAe;AACpB,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,YAAY,aAAa;AAC9B,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,UAAU,GAAG;AAAA,EAAe,KAAK,aAAa;AACnD,SAAK,OAAO,KAAK,YAAY;AAC7B,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,kBAAkB;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,gBAAyB;AACvB,UAAM,YAAY,KAAK,MAAM,MAAM,gCAAgC,KAAK,CAAC;AACzE,WAAO,UAAU,KAAK,cAAY,SAAS,QAAQ,qBAAqB,MAAM,EAAE,KAAK;AAAA,EACvF;AAAA,EAEA,eAAwB;AACtB,WAAO;AAAA,eACI,KAAK;AAAA,eACL,KAAK;AAAA,eACL,KAAK,aAAa,QAAQ,OAAO,EAAE;AAAA,eACnC,KAAK;AAAA,eACL,KAAK;AAAA;AAAA,EAElB;AACF;AAKA,IAAM,iBAAoB,CACxB,QACA,OACA,OACA,SACA,cACA,cACuB;AACvB,SAAO,IAAI,mBACT,SACA,gBAAgB,QAAQ,MAAM,GAC9B,aAAa,OAAO,OACpB,KAAK,UAAU,KAAK,GACpB,OAAO,MACP,KACF;AACF;AAEO,IAAM,UACR,CAAC,QAA0B,SAAkB,YAAmC;AACjF,iBAAgB,OAAO;AACrB,QAAI,QAAQ,KAAK;AAAG,aAAO,CAAC,OAAO,KAAK,CAAC;AACzC,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,UAAI,QAAQ;AACZ,aAAO,MAAM,IAAI,OAAK,OAAO,GAAG,GAAG,UAAU,UAAU,CAAC;AAAA,IAC1D;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,SAAS,QAAQ,MAAM;AAC1C,SAAO;AACT;AAsDK,IAAM,SACX,SAAU,OAAO;AACf,MAAI,QAAQ,KAAK;AAAG,WAAO,CAAC;AAC5B,MAAI,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,KAAK,GAAG;AAC5C,WAAO,OAAO,OAAO,CAAC,GAAG,KAAK;AAAA,EAChC;AACA,QAAM,eAAe,QAAQ,KAAK;AACpC;AAGK,IAAM,WACX,CAAC,SAAY,SAAkB,aAAoE;AACnG,mBAAkB,OAAO;AACvB,UAAM,IAAI,OAAO,KAAK;AACtB,UAAM,YAAY,OAAO,KAAK,OAAO;AACrC,UAAM,cAAc,OAAO,KAAK,CAAC,EAAE,KAAK,UAAQ,CAAC,UAAU,SAAS,IAAI,CAAC;AACzE,QAAI,aAAa;AACf,YAAM,eACJ,SACA,OACA,QACA,4BAA4B,mBAAmB,aACjD;AAAA,IACF;AACA,UAAM,YAAY,UAAU,KAAK,cAAY;AAC3C,YAAM,iBAAiB,QAAQ;AAC/B,aAAQ,eAAe,SAAS,WAAW,CAAC,EAAE,eAAe,QAAQ;AAAA,IACvE,CAAC;AACD,QAAI,WAAW;AACb,YAAM,eACJ,SACA,EAAE,YACF,GAAG,UAAU,aACb,0BAA0B,kBAAkB,eAC5C,iBAAiB,QAAQ,QAAQ,UAAU,EAAE,OAAO,CAAC,KACrD,GACF;AAAA,IACF;AAEA,UAAM,UAAU,QAAQ,KAAK,IACzB,CAAC,KAAK,QAAQ,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,CAAC,IAC/D,CAAC,KAAK,QAAQ;AACd,YAAM,SAAS,QAAQ;AACvB,UAAI,OAAO,SAAS,cAAc,CAAC,EAAE,eAAe,GAAG,GAAG;AACxD,eAAO,OAAO,OAAO,KAAK,CAAC,CAAC;AAAA,MAC9B,OAAO;AACL,eAAO,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,OAAO,EAAE,MAAM,GAAG,UAAU,KAAK,EAAE,CAAC;AAAA,MACzE;AAAA,IACF;AACF,WAAO,UAAU,OAAO,SAAS,CAAC,CAAC;AAAA,EACrC;AACA,UAAQ,OAAO,MAAM;AACnB,UAAM,QAAQ,OAAO,KAAK,OAAO,EAAE,IACjC,CAAC,QAAQ,QAAQ,KAAK,SAAS,aAC3B,GAAG,SAAS,QAAQ,QAAQ,MAAM,EAAE,QAAQ,KAAK,CAAC,MAClD,GAAG,QAAQ,QAAQ,QAAQ,IAAI,GACrC;AACA,WAAO;AAAA,GAAQ,MAAM,KAAK,OAAO;AAAA;AAAA,EACnC;AACA,SAAO;AACT;AAGO,uBAAwB,aAAqB,SAAkB,UAAkB;AACtF,SAAO,SAAU,MAAW;AAC1B,WAAO,IAAI;AACX,eAAW,OAAO,MAAM;AACtB,kBAAY,OAAO,KAAK,MAAM,GAAG,UAAU,KAAK;AAAA,IAClD;AACA,WAAO;AAAA,EACT;AACF;AAoBO,eAAgB,OAAO,SAAS,IAAI;AACzC,MAAI,QAAQ,KAAK,KAAK,QAAQ,KAAK;AAAG,WAAO;AAC7C,QAAM,eAAe,OAAO,OAAO,MAAM;AAC3C;AACA,MAAM,OAAO,MAAM;AAqBZ,IAAM,SACX,iBAAiB,OAAO,SAAS,IAAI;AACnC,MAAI,QAAQ,KAAK;AAAG,WAAO;AAC3B,MAAI,SAAS,KAAK;AAAG,WAAO;AAC5B,QAAM,eAAe,SAAQ,OAAO,MAAM;AAC5C;;;AC5UK,IAAM,4BAA4B,CAAC,UAA2B,WAAW,KAAK,KAAK;AAKnF,IAAM,oCAAoC,CAAC,UAA2B,CAAC,MAAM,SAAS,IAAI,KAAK,CAAC,MAAM,SAAS,IAAI;AAEnH,IAAM,4BAA4B,CAAC,UAA2B,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,SAAS,GAAG;AAC3G,IAAM,gCAAgC,CAAC,UAA2B,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,SAAS,GAAG;AAI/G,IAAM,cAAc,CAAC,UAA2B,MAAM,YAAY,MAAM;;;ACRxE,IAAM,8BAA8B;;;ACY3C,eAAI,2BAA2B;AAAA,EAC7B,MAAM;AAAA,EACN,SAAS;AAAA,IACP,qBAAsB,OAAO;AAC3B,aAAO;AAAA,IACT;AAAA,IACA,WAAY,OAAO,SAAS;AAC1B,aAAO,QAAQ,qBAAqB;AAAA,IACtC;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,yBAAyB;AAAA,MACvB,UAAU,CAAC,MAAM,EAAE,OAAO,WAAW;AACnC,sBAAc;AAAA,UACZ,YAAY,cAAc;AAAA,YACxB,UAAU;AAAA,YACV,OAAO;AAAA,YACP,SAAS;AAAA,UACX,CAAC;AAAA,QACH,CAAC,EAAE,IAAI;AACP,cAAM,EAAE,aAAa,KAAK;AAC1B,YAAI,SAAS,SAAS,6BAA6B;AACjD,gBAAM,IAAI,UAAU,4BAA4B,yCAAyC;AAAA,QAC3F;AACA,YAAI,CAAC,0BAA0B,QAAQ,GAAG;AACxC,gBAAM,IAAI,UAAU,kDAAkD;AAAA,QACxE;AACA,YAAI,CAAC,kCAAkC,QAAQ,GAAG;AAChD,gBAAM,IAAI,UAAU,mEAAmE;AAAA,QACzF;AACA,YAAI,CAAC,0BAA0B,QAAQ,GAAG;AACxC,gBAAM,IAAI,UAAU,+CAA+C;AAAA,QACrE;AACA,YAAI,CAAC,8BAA8B,QAAQ,GAAG;AAC5C,gBAAM,IAAI,UAAU,oDAAoD;AAAA,QAC1E;AACA,YAAI,CAAC,YAAY,QAAQ,GAAG;AAC1B,gBAAM,IAAI,UAAU,8CAA8C;AAAA,QACpE;AAAA,MACF;AAAA,MACA,QAAS,EAAE,QAAQ,EAAE,SAAS;AAC5B,cAAM,eAAe,MAAM;AAAA,UACzB,UAAU,CAAC;AAAA,UACX,YAAY,CAAC;AAAA,QACf,GAAG,IAAI;AACP,mBAAW,OAAO,cAAc;AAC9B,mBAAI,IAAI,OAAO,KAAK,aAAa,IAAI;AAAA,QACvC;AAAA,MACF;AAAA,IACF;AAAA,IACA,uCAAuC;AAAA,MACrC,UAAU;AAAA,MACV,QAAS,EAAE,QAAQ,EAAE,SAAS;AAC5B,mBAAW,OAAO,MAAM;AACtB,mBAAI,IAAI,MAAM,YAAY,KAAK,KAAK,IAAI;AAAA,QAC1C;AAAA,MACF;AAAA,IACF;AAAA,IACA,0CAA0C;AAAA,MACxC,UAAU,QAAQ,MAAM;AAAA,MACxB,QAAS,EAAE,QAAQ,EAAE,SAAS;AAC5B,mBAAW,aAAa,MAAM;AAC5B,mBAAI,OAAO,MAAM,YAAY,SAAS;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,IACA,wCAAwC;AAAA,MACtC,UAAU;AAAA,MACV,QAAS,EAAE,QAAQ,EAAE,SAAS;AAC5B,mBAAW,OAAO,MAAM;AACtB,mBAAI,IAAI,MAAM,UAAU,KAAK,KAAK,IAAI;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,IACA,uCAAuC;AAAA,MACrC,UAAU,SAAS;AAAA,QACjB,UAAU,QAAQ,MAAM;AAAA,MAC1B,CAAC;AAAA,MACD,QAAS,EAAE,QAAQ,EAAE,SAAS;AAC5B,iBAAI,IAAI,OAAO,cAAc,IAAI;AAAA,MACnC;AAAA,MACA,WAAY,EAAE,cAAc;AAE1B,YAAI,eAAe,eAAI,oBAAoB,EAAE,uBAAuB;AAGlE,yBAAI,4BAA4B,YAAY,CAAC,+CAA+C,CAAC,EAC1F,MAAM,CAAC,MAAM;AACZ,2BAAI,yBAAyB,SAAS;AAAA,cACpC,SAAS,EAAE,8HAA8H;AAAA,gBACvI,SAAS,EAAE;AAAA,gBACX,QAAQ,EAAE,WAAW;AAAA,cACvB,CAAC;AAAA,YACH,CAAC;AAAA,UACH,CAAC;AAAA,QACL;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "// Matches strings of plain ASCII letters and digits, hyphens and underscores.\nexport const allowedUsernameCharacters = (value ) => /^[\\w-]*$/.test(value)\n\n// Matches non-empty strings of plain ASCII letters and digits.\nexport const alphanumeric = (value ) => /^[A-Za-z\\d]+$/.test(value)\n\nexport const noConsecutiveHyphensOrUnderscores = (value ) => !value.includes('--') && !value.includes('__')\n\nexport const noLeadingOrTrailingHyphen = (value ) => !value.startsWith('-') && !value.endsWith('-')\nexport const noLeadingOrTrailingUnderscore = (value ) => !value.startsWith('_') && !value.endsWith('_')\n\nexport const decimals = (digits ) => (value ) => Number.isInteger(value * Math.pow(10, digits))\n\nexport const noUppercase = (value ) => value.toLowerCase() === value\n\nexport const noWhitespace = (value ) => /^\\S+$/.test(value)\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\n\nimport { Vue, L } from '@common/common.js'\nimport { merge } from './shared/giLodash.js'\nimport { objectOf, objectMaybeOf, arrayOf, string, object } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n allowedUsernameCharacters,\n noConsecutiveHyphensOrUnderscores,\n noLeadingOrTrailingHyphen,\n noLeadingOrTrailingUnderscore,\n noUppercase\n} from './shared/validators.js'\n\nimport { IDENTITY_USERNAME_MAX_CHARS } from './shared/constants.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/identity',\n getters: {\n currentIdentityState (state) {\n return state\n },\n loginState (state, getters) {\n return getters.currentIdentityState.loginState\n }\n },\n actions: {\n 'gi.contracts/identity': {\n validate: (data, { state, meta }) => {\n objectMaybeOf({\n attributes: objectMaybeOf({\n username: string,\n email: string,\n picture: string\n })\n })(data)\n const { username } = data.attributes\n if (username.length > IDENTITY_USERNAME_MAX_CHARS) {\n throw new TypeError(`A username cannot exceed ${IDENTITY_USERNAME_MAX_CHARS} characters.`)\n }\n if (!allowedUsernameCharacters(username)) {\n throw new TypeError('A username cannot contain disallowed characters.')\n }\n if (!noConsecutiveHyphensOrUnderscores(username)) {\n throw new TypeError('A username cannot contain two consecutive hyphens or underscores.')\n }\n if (!noLeadingOrTrailingHyphen(username)) {\n throw new TypeError('A username cannot start or end with a hyphen.')\n }\n if (!noLeadingOrTrailingUnderscore(username)) {\n throw new TypeError('A username cannot start or end with an underscore.')\n }\n if (!noUppercase(username)) {\n throw new TypeError('A username cannot contain uppercase letters.')\n }\n },\n process ({ data }, { state }) {\n const initialState = merge({\n settings: {},\n attributes: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/identity/setAttributes': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.attributes, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/deleteAttributes': {\n validate: arrayOf(string),\n process ({ data }, { state }) {\n for (const attribute of data) {\n Vue.delete(state.attributes, attribute)\n }\n }\n },\n 'gi.contracts/identity/updateSettings': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/setLoginState': {\n validate: objectOf({\n groupIds: arrayOf(string)\n }),\n process ({ data }, { state }) {\n Vue.set(state, 'loginState', data)\n },\n sideEffect ({ contractID }) {\n // it only makes sense to call updateLoginStateUponLogin for ourselves\n if (contractID === sbp('state/vuex/getters').ourIdentityContractId) {\n // makes sure that updateLoginStateUponLogin gets run after the entire identity\n // state has been synced, this way we don't end up joining groups we've left, etc.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/updateLoginStateUponLogin'])\n .catch((e) => {\n sbp('gi.notifications/emit', 'ERROR', {\n message: L(\"Failed to join groups we're part of on another device. Not catastrophic, but could lead to problems. {errName}: '{errMsg}'\", {\n errName: e.name,\n errMsg: e.message || '?'\n })\n })\n })\n }\n }\n }\n }\n})\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/mailbox.js b/test/contracts/mailbox.js index 6c0b51bdf7..f37c3df80a 100644 --- a/test/contracts/mailbox.js +++ b/test/contracts/mailbox.js @@ -359,14 +359,14 @@ var objectOf = (typeObj, _scope = "Object") => { } const undefAttr = typeAttrs.find((property) => { const propertyTypeFn = typeObj[property]; - return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); }); if (undefAttr) { throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); } const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { const typeFn = typeObj[key]; - if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { return Object.assign(acc, {}); } else { return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); @@ -375,7 +375,10 @@ var objectOf = (typeObj, _scope = "Object") => { return typeAttrs.reduce(reducer, {}); } object2.type = () => { - const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); return `{| ${props.join(",\n ")} |}`; diff --git a/test/contracts/mailbox.js.map b/test/contracts/mailbox.js.map index 0b239b5707..25423b0e63 100644 --- a/test/contracts/mailbox.js.map +++ b/test/contracts/mailbox.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/mailbox.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { mailType } from './shared/types.js'\nimport { objectOf, string, object, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/mailbox',\n metadata: {\n // TODO: why is this missing the from username..?\n validate: objectOf({\n createdDate: string\n }),\n create () {\n return {\n createdDate: new Date().toISOString()\n }\n }\n },\n actions: {\n 'gi.contracts/mailbox': {\n validate: object, // TODO: define this\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state, key, data[key])\n }\n Vue.set(state, 'messages', [])\n }\n },\n 'gi.contracts/mailbox/postMessage': {\n validate: objectOf({\n messageType: mailType,\n from: string,\n subject: optional(string),\n message: optional(string),\n headers: optional(object)\n }),\n process (message, { state }) {\n state.messages.push(message)\n }\n },\n 'gi.contracts/mailbox/authorizeSender': {\n validate: objectOf({\n sender: string\n }),\n process ({ data }, { state }) {\n // TODO: replace this via OP_KEY_*?\n throw new Error('unimplemented!')\n }\n }\n }\n})\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { mailType } from './shared/types.js'\nimport { objectOf, string, object, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/mailbox',\n metadata: {\n // TODO: why is this missing the from username..?\n validate: objectOf({\n createdDate: string\n }),\n create () {\n return {\n createdDate: new Date().toISOString()\n }\n }\n },\n actions: {\n 'gi.contracts/mailbox': {\n validate: object, // TODO: define this\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state, key, data[key])\n }\n Vue.set(state, 'messages', [])\n }\n },\n 'gi.contracts/mailbox/postMessage': {\n validate: objectOf({\n messageType: mailType,\n from: string,\n subject: optional(string),\n message: optional(string),\n headers: optional(object)\n }),\n process (message, { state }) {\n state.messages.push(message)\n }\n },\n 'gi.contracts/mailbox/authorizeSender': {\n validate: objectOf({\n sender: string\n }),\n process ({ data }, { state }) {\n // TODO: replace this via OP_KEY_*?\n throw new Error('unimplemented!')\n }\n }\n }\n})\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/shared/payments/index.js.map b/test/contracts/shared/payments/index.js.map index 114ff938c7..527eb79258 100644 --- a/test/contracts/shared/payments/index.js.map +++ b/test/contracts/shared/payments/index.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../../../frontend/model/contracts/misc/flowTyper.js", "../../../../frontend/model/contracts/shared/payments/index.js"], - "sourcesContent": ["// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n"], - "mappings": "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", + "sourcesContent": ["// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/shared/voting/proposals.js b/test/contracts/shared/voting/proposals.js index 4312b4f4d7..b4185abda4 100644 --- a/test/contracts/shared/voting/proposals.js +++ b/test/contracts/shared/voting/proposals.js @@ -332,14 +332,14 @@ var objectOf = (typeObj, _scope = "Object") => { } const undefAttr = typeAttrs.find((property) => { const propertyTypeFn = typeObj[property]; - return propertyTypeFn.name === "maybe" && !o.hasOwnProperty(property); + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); }); if (undefAttr) { throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); } const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { const typeFn = typeObj[key]; - if (typeFn.name === "optional" && !o.hasOwnProperty(key)) { + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { return Object.assign(acc, {}); } else { return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); @@ -348,7 +348,10 @@ var objectOf = (typeObj, _scope = "Object") => { return typeAttrs.reduce(reducer, {}); } object2.type = () => { - const props = Object.keys(typeObj).map((key) => typeObj[key].name === "optional" ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`); + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); return `{| ${props.join(",\n ")} |}`; diff --git a/test/contracts/shared/voting/proposals.js.map b/test/contracts/shared/voting/proposals.js.map index a595b17598..a791210663 100644 --- a/test/contracts/shared/voting/proposals.js.map +++ b/test/contracts/shared/voting/proposals.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../../../node_modules/@sbp/sbp/dist/module.mjs", "../../../../frontend/common/common.js", "../../../../frontend/common/vSafeHtml.js", "../../../../frontend/model/contracts/shared/giLodash.js", "../../../../frontend/common/translations.js", "../../../../frontend/common/stringTemplate.js", "../../../../frontend/model/contracts/misc/flowTyper.js", "../../../../frontend/model/contracts/shared/time.js", "../../../../frontend/model/contracts/shared/constants.js", "../../../../frontend/model/contracts/shared/voting/rules.js", "../../../../frontend/model/contracts/shared/voting/proposals.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name === 'maybe' && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name === 'optional' && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => typeObj[key].name === 'optional'\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n"], + "mappings": "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", "names": [] } From 67cff98a6cbe507dfb976fb38ba97567f8fccacf Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Wed, 21 Sep 2022 12:47:25 +0200 Subject: [PATCH 24/43] Add exec:ts task (deno check) in the build --- Gruntfile.js | 19 ++++++---- backend/index.ts | 18 +++++---- backend/pubsub.ts | 2 +- backend/types.ts | 37 ------------------- backend/auth.ts => historical/backend/auth.js | 21 +++++------ test/backend.test.ts | 7 ++-- 6 files changed, 37 insertions(+), 67 deletions(-) delete mode 100644 backend/types.ts rename backend/auth.ts => historical/backend/auth.js (62%) diff --git a/Gruntfile.js b/Gruntfile.js index 08c381fcf5..d8c93f5745 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -69,16 +69,20 @@ const { } = process.env const backendIndex = './backend/index.ts' +const contractsDir = 'frontend/model/contracts' +const denoImportMap = 'import-map.json' +const denoRunPermissions = ['--allow-env', '--allow-net', '--allow-read', '--allow-write'] +const denoTestPermissions = ['--allow-env', '--allow-net', '--allow-read', '--allow-write'] const distAssets = 'dist/assets' const distCSS = 'dist/assets/css' const distDir = 'dist' const distContracts = 'dist/contracts' const distJS = 'dist/assets/js' -const serviceWorkerDir = 'frontend/controller/serviceworkers' +const manifestJSON = path.join(contractsDir, 'manifests.json') const srcDir = 'frontend' -const contractsDir = 'frontend/model/contracts' +const serviceWorkerDir = 'frontend/controller/serviceworkers' + const mainSrc = path.join(srcDir, 'main.js') -const manifestJSON = path.join(contractsDir, 'manifests.json') const development = NODE_ENV === 'development' const production = !development @@ -386,6 +390,7 @@ module.exports = (grunt) => { }, exec: { + chelDeployAll: 'find contracts -iname "*.manifest.json" | xargs -r ./node_modules/.bin/chel deploy ./data', eslint: 'node ./node_modules/eslint/bin/eslint.js --cache "**/*.{js,vue}"', flow: '"./node_modules/.bin/flow" --quiet || echo The Flow check failed!', puglint: '"./node_modules/.bin/pug-lint-vue" frontend/views', @@ -397,8 +402,8 @@ module.exports = (grunt) => { options: { env: process.env } }, // Test anything in /test that ends with `.test.ts`. - testWithDeno: 'deno test --allow-env --allow-net --allow-read --allow-write --import-map=import-map.json --no-check ./test/*.ts', - chelDeployAll: 'find contracts -iname "*.manifest.json" | xargs -r ./node_modules/.bin/chel deploy ./data' + testWithDeno: `deno test ${denoTestPermissions.join(' ')} --import-map=${denoImportMap} --no-check ./test/*.test.ts`, + ts: `deno check --import-map=${denoImportMap} backend/*.ts shared/*.ts shared/domains/chelonia/*.ts` } }) @@ -412,7 +417,7 @@ module.exports = (grunt) => { const esbuild = this.flags.watch ? 'esbuild:watch' : 'esbuild' if (!grunt.option('skipbuild')) { - grunt.task.run(['exec:eslint', 'exec:flow', 'exec:puglint', 'exec:stylelint', 'clean', 'copy', esbuild]) + grunt.task.run(['exec:eslint', 'exec:flow', 'exec:puglint', 'exec:stylelint', 'exec:ts', 'clean', 'copy', esbuild]) } }) @@ -477,7 +482,7 @@ module.exports = (grunt) => { const done = this.async() // Tell Grunt we're async. child = spawn( 'deno', - ['run', '--allow-env', '--allow-net', '--allow-read', '--allow-write', '--import-map=import-map.json', backendIndex], + ['run', ...denoRunPermissions, `--import-map=${denoImportMap}`, backendIndex], { stdio: 'inherit' } diff --git a/backend/index.ts b/backend/index.ts index d093507a76..0ce2c2d4c0 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -45,9 +45,9 @@ export default (new Promise((resolve, reject) => { } })) -const shutdownFn = function () { +const shutdownFn = (): void => { sbp('okTurtles.data/apply', PUBSUB_INSTANCE, function (pubsub: PubsubServer) { - console.log('message received in child, shutting down...') + console.log('signal received in child, shutting down...') pubsub.on('close', async function () { try { await sbp('backend/server/stop') @@ -59,14 +59,18 @@ const shutdownFn = function () { } }) pubsub.close() - // Since `ws` v8.0, `WebSocketServer.close()` no longer closes remaining connections. - // See https://github.com/websockets/ws/commit/df7de574a07115e2321fdb5fc9b2d0fea55d27e8 - pubsub.clients.forEach((client: PubsubClient) => client.terminate()) }) } -// Deno.addSignalListener('SIGBREAK', shutdownFn) -Deno.addSignalListener('SIGINT', shutdownFn) +/* + On Windows only SIGINT (ctrl+c) and SIGBREAK (ctrl+break) are supported, but + SIGBREAK is not supported on Linux. +*/ +['SIGBREAK', 'SIGINT'].forEach(signal => { + try { + Deno.addSignalListener(signal as Deno.Signal, shutdownFn) + } catch {} +}) // Equivalent to the `uncaughtException` event in Nodejs. addEventListener('error', (event) => { diff --git a/backend/pubsub.ts b/backend/pubsub.ts index 5c93d9f40d..a2df1c7dea 100644 --- a/backend/pubsub.ts +++ b/backend/pubsub.ts @@ -10,7 +10,7 @@ interface Message { type: string } -type MessageHandler = (this: PubsubServer, msg: Message) => void +type MessageHandler = (this: PubsubClient, msg: Message) => void type PubsubClientEventName = 'close' | 'message' type PubsubServerEventName = 'close' | 'connection' | 'error' | 'headers' | 'listening' diff --git a/backend/types.ts b/backend/types.ts deleted file mode 100644 index b23cf32c39..0000000000 --- a/backend/types.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Request from './request.ts' -import ServerResponse from './response.ts' -import Toolkit from './toolkit.ts' - -type JSONStringifyable = boolean | null | number | object | string - -export interface MatchedRoute extends NormalizedRoute { - params: RequestParams -} - -export interface NormalizedRoute extends Route { - paramNames: Array, - segments: Array -} - -export type RequestParams = { [param: string]: string } -export type RequestState = { [name: string]: string } - -export type ResponseBody = Deno.Reader | Uint8Array | JSONStringifyable - -export interface Route { - method: string, - path: string, - handler: RouteHandler, - vhost?: string -} -export type RouteHandler = (request: Request, h: Toolkit) => RouteHandlerResult -export type RouteHandlerResult = ServerResponse | ResponseBody | Error | Promise - -export interface RouteOptions extends Omit, 'method' | 'path'> { - method?: Route['method'] | Iterable, - path?: Route['path'] | Iterable -} - -export type { DirectoryHandlerOptions } from './helpers/directory.tsx' -export type { FileHandlerOptions } from './helpers/file.ts' -export type { ServerOptions } from './server.ts' diff --git a/backend/auth.ts b/historical/backend/auth.js similarity index 62% rename from backend/auth.ts rename to historical/backend/auth.js index 78a94df3e6..dfd58f22c5 100644 --- a/backend/auth.ts +++ b/historical/backend/auth.js @@ -1,37 +1,36 @@ -/* globals Deno */ -// Create our auth plugin. See: +// create our auth plugin. see: // https://hapijs.com/tutorials/auth // https://hapijs.com/tutorials/plugins -import { verify, b64ToStr } from '~/shared/functions.ts' +import { verify, b64ToStr } from '~/shared/functions.js' -const { PermissionDenied } = Deno.errors +const Boom = require('@hapi/boom') -export default { +exports.plugin = { name: 'gi-auth', - register (server: unknown, opts: unknown) { + register: function (server: Object, opts: Object) { server.auth.scheme('gi-auth', function (server, options) { return { - authenticate (request, h) { + authenticate: function (request, h) { const { authorization } = request.headers - if (!authorization) h.unauthenticated(new PermissionDenied('Missing authorization')) + if (!authorization) h.unauthenticated(Boom.unauthorized('Missing authorization')) let [scheme, json] = authorization.split(/\s+/) // NOTE: if you want to add any signature verification, do it here // eslint-disable-next-line no-constant-condition if (false) { - if (!scheme.includes('gi')) h.unauthenticated(new PermissionDenied('Bad authentication')) + if (!scheme.includes('gi')) h.unauthenticated(Boom.badRequest('Bad authentication')) try { json = JSON.parse(b64ToStr(json)) } catch (e) { - return h.unauthenticated(new PermissionDenied('Invalid token format')) + return h.unauthenticated(Boom.badRequest('Invalid token format')) } // http://hapijs.com/api/#serverauthschemename-scheme const isValid = verify(json.msg, json.key, json.sig) json.userId = json.key const credentials = { credentials: json } - if (!isValid) return h.unauthenticated(new PermissionDenied('Bad credentials'), credentials) + if (!isValid) return h.unauthenticated(Boom.unauthorized('Bad credentials'), credentials) return h.authenticated(credentials) } else { // remove this if you decide to implement it diff --git a/test/backend.test.ts b/test/backend.test.ts index 1be2983dad..4df0cea5c9 100644 --- a/test/backend.test.ts +++ b/test/backend.test.ts @@ -20,6 +20,9 @@ import { createInvite } from './contracts/shared/functions.js' import '~/frontend/controller/namespace.js' import { THEME_LIGHT } from '~/frontend/utils/themes.js' import manifests from '~/frontend/model/contracts/manifests.json' assert { type: "json" } +import packageJSON from '~/package.json' assert { type: 'json' } + +const { version } = packageJSON // var unsignedMsg = sign(personas[0], 'futz') @@ -54,10 +57,6 @@ const applyPortShift = (env) => { Object.assign(process.env, applyPortShift(process.env)) -const { default: { version }} = await import('~/package.json', { - assert: { type: "json" }, -}) - Deno.env.set('GI_VERSION', `${version}@${new Date().toISOString()}`) const API_PORT = Deno.env.get('API_PORT') From 7a2c8c701d67d2206adb91cbff37c48b249b8a80 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 9 Oct 2022 11:25:27 +0200 Subject: [PATCH 25/43] Fix Flow errors --- .flowconfig | 2 + frontend/controller/actions/group.js | 2 +- frontend/controller/actions/mailbox.js | 2 +- frontend/controller/backend.js | 2 +- frontend/controller/e2e/keys.js | 2 +- frontend/main.js | 2 +- frontend/model/notifications/templates.js | 2 +- package.json | 10 ++--- shared/declarations.js | 2 - shared/domains/chelonia/types.flow.js | 45 +++++++++++++++++++++++ shared/types.flow.js | 3 ++ 11 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 shared/domains/chelonia/types.flow.js create mode 100644 shared/types.flow.js diff --git a/.flowconfig b/.flowconfig index fd8b7c36e5..636735a325 100644 --- a/.flowconfig +++ b/.flowconfig @@ -11,6 +11,8 @@ .*/backend/.* # Shared files must be imported by Deno backend files, so they better not contain Flowtype annotations. .*/shared/.* +!.*/shared/types.flow.js +!.*/shared/.*/types.flow.js .*/dist/.* .*/contracts/.* .*/frontend/assets/.* diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 1664b84129..3591719f99 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -23,7 +23,7 @@ import { dateToPeriodStamp, addTimeToDate, DAYS_MILLIS } from '@model/contracts/ import { encryptedAction } from './utils.js' import { VOTE_FOR } from '@model/contracts/shared/voting/rules.js' import type { GIActionParams } from './types.js' -import type { GIMessage } from '~/shared/domains/chelonia/chelonia.ts' +import type { GIMessage } from '~/shared/domains/chelonia/types.flow.js' export async function leaveAllChatRooms (groupContractID: string, member: string) { // let user leaves all the chatrooms before leaving group diff --git a/frontend/controller/actions/mailbox.js b/frontend/controller/actions/mailbox.js index 88bc3f0292..e7471b952d 100644 --- a/frontend/controller/actions/mailbox.js +++ b/frontend/controller/actions/mailbox.js @@ -3,7 +3,7 @@ import sbp from '@sbp/sbp' import { GIErrorUIRuntimeError, L, LError } from '@common/common.js' import { encryptedAction } from './utils.js' -import type { GIMessage } from '~/shared/domains/chelonia/chelonia.ts' +import type { GIMessage } from '~/shared/domains/chelonia/types.flow.js' export default (sbp('sbp/selectors/register', { 'gi.actions/mailbox/create': async function ({ diff --git a/frontend/controller/backend.js b/frontend/controller/backend.js index c680c86e5c..adffb3dcb8 100644 --- a/frontend/controller/backend.js +++ b/frontend/controller/backend.js @@ -1,6 +1,6 @@ 'use strict' -import type { JSONObject } from '~/shared/types.ts' +import type { JSONObject } from '~/shared/types.flow.js' import sbp from '@sbp/sbp' import { NOTIFICATION_TYPE } from '~/shared/pubsub.ts' diff --git a/frontend/controller/e2e/keys.js b/frontend/controller/e2e/keys.js index e76e8d2cac..00163bb8e3 100644 --- a/frontend/controller/e2e/keys.js +++ b/frontend/controller/e2e/keys.js @@ -7,7 +7,7 @@ import { blake32Hash, bytesToB64 } from '~/shared/functions.ts' import nacl from 'tweetnacl' import scrypt from 'scrypt-async' -import type { GIKey, GIKeyType } from '~/shared/domains/chelonia/GIMessage.ts' +import type { GIKey, GIKeyType } from '~/shared/domains/chelonia/types.flow.js' function genSeed (): string { return bytesToB64(nacl.randomBytes(nacl.box.secretKeyLength)) diff --git a/frontend/main.js b/frontend/main.js index dee3bcee72..81b50ce479 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -10,7 +10,7 @@ import { mapMutations, mapGetters, mapState } from 'vuex' import 'wicg-inert' import '@model/captureLogs.js' -import type { GIMessage } from '~/shared/domains/chelonia/chelonia.ts' +import type { GIMessage } from '~/shared/domains/chelonia/types.flow.js' import '~/shared/domains/chelonia/chelonia.ts' import { CONTRACT_IS_SYNCING } from '~/shared/domains/chelonia/events.ts' import * as Common from '@common/common.js' diff --git a/frontend/model/notifications/templates.js b/frontend/model/notifications/templates.js index e55c0770dd..d524535a6a 100644 --- a/frontend/model/notifications/templates.js +++ b/frontend/model/notifications/templates.js @@ -1,4 +1,4 @@ -import type { GIMessage } from '~/shared/domains/chelonia/chelonia.ts' +import type { GIMessage } from '~/shared/domains/chelonia/types.flow.js' import type { NewProposalType, NotificationTemplate diff --git a/package.json b/package.json index 7e9d368163..be67dca1fb 100644 --- a/package.json +++ b/package.json @@ -48,18 +48,18 @@ } }, "eslintIgnore": [ + "contracts/*", + "dist/*", "frontend/assets/*", "frontend/model/contracts/misc/flowTyper.js", - "shared/declarations.js", "historical/*", - "shared/types.js", - "dist/*", "ignored/*", "node_modules/*", + "shared/**/*.flow.js", + "shared/declarations.js", "test/common/*", "test/contracts/*", - "test/cypress/cache/*", - "contracts/*" + "test/cypress/cache/*" ], "browserslist": "> 1% and since 2018 and not dead", "stylelint": { diff --git a/shared/declarations.js b/shared/declarations.js index 6e270b7c1e..299d0bb108 100644 --- a/shared/declarations.js +++ b/shared/declarations.js @@ -67,14 +67,12 @@ declare module '~/frontend/model/contracts/misc/flowTyper.js' { declare module.e declare module '~/frontend/model/contracts/shared/time.js' { declare module.exports: Object } declare module '@model/contracts/shared/time.js' { declare module.exports: Object } // HACK: declared some shared files below but not sure why it's necessary -declare module '~/shared/domains/chelonia/GIMessage.ts' { declare module.exports: Object } declare module '~/shared/domains/chelonia/chelonia.ts' { declare module.exports: any } declare module '~/shared/domains/chelonia/errors.ts' { declare module.exports: Object } declare module '~/shared/domains/chelonia/events.ts' { declare module.exports: Object } declare module '~/shared/domains/chelonia/internals.ts' { declare module.exports: Object } declare module '~/shared/functions.ts' { declare module.exports: any } declare module '~/shared/pubsub.ts' { declare module.exports: any } -declare module '~/shared/types.js' { declare module.exports: any } declare module '~/frontend/model/contracts/shared/giLodash.js' { declare module.exports: any } declare module '@model/contracts/shared/giLodash.js' { declare module.exports: any } diff --git a/shared/domains/chelonia/types.flow.js b/shared/domains/chelonia/types.flow.js new file mode 100644 index 0000000000..364a59543b --- /dev/null +++ b/shared/domains/chelonia/types.flow.js @@ -0,0 +1,45 @@ +import type { JSONArray, JSONObject, JSONType } from '~/shared/types.flow.js' + +export type GIKeyType = '' + +export type GIKey = { + type: GIKeyType; + data: Object; // based on GIKeyType this will change + meta: Object; +} +// Allows server to check if the user is allowed to register this type of contract +// TODO: rename 'type' to 'contractName': +export type GIOpContract = { type: string; keyJSON: string, parentContract?: string } +export type GIOpActionEncrypted = string // encrypted version of GIOpActionUnencrypted +export type GIOpActionUnencrypted = { action: string; data: JSONType; meta: JSONObject } +export type GIOpKeyAdd = { keyHash: string, keyJSON: ?string, context: string } +export type GIOpPropSet = { key: string, value: JSONType } + +export type GIOpType = 'c' | 'ae' | 'au' | 'ka' | 'kd' | 'pu' | 'ps' | 'pd' +export type GIOpValue = GIOpContract | GIOpActionEncrypted | GIOpActionUnencrypted | GIOpKeyAdd | GIOpPropSet +export type GIOp = [GIOpType, GIOpValue] + +export type GIMessage = { + _decrypted: GIOpValue; + _mapping: Object; + _message: Object; + + decryptedValue (fn?: Function): any; + + message (): Object; + + op (): GIOp; + + opType (): GIOpType; + + opValue (): GIOpValue; + + manifest (): string; + + description (): string; + + isFirstMessage (): boolean; + contractID (): string; + serialize (): string; + hash (): string; +} diff --git a/shared/types.flow.js b/shared/types.flow.js new file mode 100644 index 0000000000..8beae8a6bb --- /dev/null +++ b/shared/types.flow.js @@ -0,0 +1,3 @@ +export type JSONType = string | number | boolean | null | JSONObject | JSONArray +export interface JSONObject { [key: string]: JSONType } +export type JSONArray = Array From 6f9a3c9c6d881a9624ad007c8376ea8761da4823 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Mon, 17 Oct 2022 13:02:11 +0200 Subject: [PATCH 26/43] Migrate i18n validation tests to Deno --- import-map.json | 2 + test/validate-i18n.test.js | 214 ----------------------------------- test/validate-i18n.test.ts | 225 +++++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+), 214 deletions(-) delete mode 100644 test/validate-i18n.test.js create mode 100644 test/validate-i18n.test.ts diff --git a/import-map.json b/import-map.json index dc56649751..9fee69ac44 100644 --- a/import-map.json +++ b/import-map.json @@ -9,6 +9,7 @@ "fmt/": "https://deno.land/std@0.138.0/fmt/", "path": "https://deno.land/std@0.138.0/path/mod.ts", + "asserts": "https://deno.land/std@0.153.0/testing/asserts.ts", "blakejs": "https://cdn.skypack.dev/blakejs", "buffer": "https://cdn.skypack.dev/buffer", "dompurify": "https://cdn.skypack.dev/dompurify", @@ -16,6 +17,7 @@ "multihashes": "https://esm.sh/multihashes", "pogo": "https://raw.githubusercontent.com/snowteamer/pogo/master/main.ts", "pogo/": "https://raw.githubusercontent.com/snowteamer/pogo/master/", + "pug-lint": "https://esm.sh/pug-lint", "tweetnacl": "https://cdn.skypack.dev/tweetnacl", "vue": "https://esm.sh/vue@2.7.10" } diff --git a/test/validate-i18n.test.js b/test/validate-i18n.test.js deleted file mode 100644 index 9505cc90e5..0000000000 --- a/test/validate-i18n.test.js +++ /dev/null @@ -1,214 +0,0 @@ -'use strict' - -const assert = require('assert') - -const PugLinter = require('pug-lint') - -const linter = new PugLinter() - -linter.configure( - { - additionalRules: ['scripts/validate-i18n.js'], - validateI18n: true - } -) - -const errorCode = 'PUG:LINT_VALIDATEI18N' - -function outdent (str) { - const lines = str.slice(1).split('\n') - const indent = (lines[0].match(/^\s*/) || [''])[0] - - if (indent === '') { - return lines.join('\n') - } - return lines.map( - line => line.startsWith(indent) ? line.slice(indent.length) : line - ).join('\n') -} - -it('should allow valid usage of a simple string', function () { - const validUseCase = 'i18n Hello world' - - assert.equal(linter.checkString(validUseCase).length, 0, validUseCase) -}) - -it('should allow valid usage of a string with named arguments', function () { - const validUseCase = outdent( - String.raw` - i18n( - :args='{ name: ourUsername }' - ) Hello {name}!` - ) - - assert.equal(linter.checkString(validUseCase).length, 0, validUseCase) -}) - -it('should allow valid usage of a string containing HTML markup', function () { - const validUseCase = outdent( - String.raw` - i18n( - :args='{ ...LTags("strong", "span"), name: ourUsername }' - ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!` - ) - - assert.equal(linter.checkString(validUseCase).length, 0, validUseCase) -}) - -it('should allow valid usage of a string continaing Vue markup', function () { - const validUseCase = outdent( - String.raw` - i18n( - compile - :args='{ r1: \'\', r2: ""}' - ) Go to {r1}login{r2} page.` - ) - - assert.equal(linter.checkString(validUseCase).length, 0, validUseCase) -}) - -// ====== Invalid usages ====== // - -it('should report syntax errors in the `:args` attribute', function () { - let errors = [] - let invalidUseCase = '' - - // Example with a missing colon. - invalidUseCase = outdent( - String.raw` - i18n( - :args='{ name ourUsername }' - ) Hello {name}!` - ) - - errors = linter.checkString(invalidUseCase) - assert.equal(errors.length, 2) - assert.equal(errors[0].code, errorCode) - assert.match(errors[0].message, /unexpected token/i) - - assert.equal(errors[1].code, errorCode) - assert.match(errors[1].message, /undefined named argument 'name'/i) - - // Example with a missing curly brace. - invalidUseCase = outdent( - String.raw` - i18n( - :args='{ name: ourUsername' - ) Hello {name}!` - ) - - errors = linter.checkString(invalidUseCase) - - assert.equal(errors.length, 2) - assert.equal(errors[0].code, errorCode) - assert.match(errors[0].message, /unexpected token/i) - - assert.equal(errors[1].code, errorCode) - assert.match(errors[1].message, /undefined named argument 'name'/i) - - // Example with an extraneous semicolon. - invalidUseCase = outdent( - String.raw` - i18n( - :args='{ name: ourUsername };' - ) Hello {name}!` - ) - - errors = linter.checkString(invalidUseCase) - assert.equal(errors.length, 1) - - // Example with an invalid property key. - invalidUseCase = outdent( - String.raw` - i18n( - :args='{ 0name: ourUsername };' - ) Hello {name}!` - ) - - errors = linter.checkString(invalidUseCase) - assert(linter.checkString(invalidUseCase).length > 0, invalidUseCase) -}) - -it('should report undefined ltags', function () { - const invalidUseCase = outdent( - String.raw` - i18n( - :args='{ count: 5 }' - ) Invite {strong_}{count} members{_strong} to the party!` - ) - const errors = linter.checkString(invalidUseCase) - - assert.equal(errors.length, 2) - assert.equal(errors[0].code, errorCode) - assert.match(errors[0].message, /undefined named argument 'strong_'/i) - assert.equal(errors[1].code, errorCode) - assert.match(errors[1].message, /undefined named argument '_strong'/i) -}) - -it('should report undefined named arguments', function () { - const invalidUseCase = outdent( - String.raw` - i18n( - :args='{ ...LTags("strong") }' - ) Invite {strong_}{count} members{_strong} to the party!` - ) - const errors = linter.checkString(invalidUseCase) - - assert.equal(errors.length, 1) - assert.equal(errors[0].code, errorCode) - assert.match(errors[0].message, /undefined named argument 'count'/i) -}) - -it('should report unused ltags', function () { - const invalidUseCase = outdent( - String.raw` - i18n( - :args='{ ...LTags("strong") }' - ) Invite your friends to the party!` - ) - const errors = linter.checkString(invalidUseCase) - - assert.equal(errors.length, 2) - assert.equal(errors[0].code, errorCode) - assert.match(errors[0].message, /unused named argument 'strong_'/i) - assert.equal(errors[1].code, errorCode) - assert.match(errors[1].message, /unused named argument '_strong'/i) -}) - -it('should report unused named arguments', function () { - const invalidUseCase = outdent( - String.raw` - i18n( - :args='{ age, name }' - ) Hello {name}!` - ) - const errors = linter.checkString(invalidUseCase) - - assert.equal(errors.length, 1) - assert.equal(errors[0].code, errorCode) - assert.match(errors[0].message, /unused named argument 'age'/i) -}) - -it('should report usage of the `html` attribute', function () { - const invalidUseCase = outdent( - String.raw` - i18n( - tag='p' - html='My great text' - ) Hello` - ) - - const errors = linter.checkString(invalidUseCase) - assert.equal(errors.length, 1) - assert.equal(errors[0].code, errorCode) - assert.match(errors[0].message, /html attribute/) -}) - -it('should report usage of double curly braces', function () { - const invalidUseCase = 'i18n Replying to {{replyingTo}}' - const errors = linter.checkString(invalidUseCase) - - assert.equal(errors.length, 1) - assert.equal(errors[0].code, errorCode) - assert.match(errors[0].message, /double curly braces/i) -}) diff --git a/test/validate-i18n.test.ts b/test/validate-i18n.test.ts new file mode 100644 index 0000000000..53e978ef15 --- /dev/null +++ b/test/validate-i18n.test.ts @@ -0,0 +1,225 @@ +import { + assert, + assertEquals, + assertMatch, +} from 'asserts' + +import { createRequire } from "https://deno.land/std/node/module.ts"; +import PugLinter from 'pug-lint' + +// HACK for 'dynamic require is not supported' error in 'linter.configure()'. +globalThis.require = createRequire(import.meta.url); + +Deno.test({ + name: 'i18n tag validation', + fn: async function (tests) { + const linter = new PugLinter() + + linter.configure( + { + additionalRules: ['scripts/validate-i18n.js'], + validateI18n: true + } + ) + + const errorCode = 'PUG:LINT_VALIDATEI18N' + + function outdent (str) { + const lines = str.slice(1).split('\n') + const indent = (lines[0].match(/^\s*/) || [''])[0] + + if (indent === '') { + return lines.join('\n') + } + return lines.map( + line => line.startsWith(indent) ? line.slice(indent.length) : line + ).join('\n') + } + + await tests.step('should allow valid usage of a simple string', async function () { + const validUseCase = 'i18n Hello world' + + assertEquals(linter.checkString(validUseCase).length, 0, validUseCase) + }) + + await tests.step('should allow valid usage of a string with named arguments', async function () { + const validUseCase = outdent( + String.raw` + i18n( + :args='{ name: ourUsername }' + ) Hello {name}!` + ) + + assertEquals(linter.checkString(validUseCase).length, 0, validUseCase) + }) + + await tests.step('should allow valid usage of a string containing HTML markup', async function () { + const validUseCase = outdent( + String.raw` + i18n( + :args='{ ...LTags("strong", "span"), name: ourUsername }' + ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!` + ) + + assertEquals(linter.checkString(validUseCase).length, 0, validUseCase) + }) + + await tests.step('should allow valid usage of a string containing Vue markup', async function () { + const validUseCase = outdent( + String.raw` + i18n( + compile + :args='{ r1: \'\', r2: ""}' + ) Go to {r1}login{r2} page.` + ) + + assertEquals(linter.checkString(validUseCase).length, 0, validUseCase) + }) + + // ====== Invalid usages ====== // + + await tests.step('should report syntax errors in the `:args` attribute', async function () { + let errors = [] + let invalidUseCase = '' + + // Example with a missing colon. + invalidUseCase = outdent( + String.raw` + i18n( + :args='{ name ourUsername }' + ) Hello {name}!` + ) + + errors = linter.checkString(invalidUseCase) + assertEquals(errors.length, 2) + assertEquals(errors[0].code, errorCode) + assertMatch(errors[0].message, /unexpected token/i) + + assertEquals(errors[1].code, errorCode) + assertMatch(errors[1].message, /undefined named argument 'name'/i) + + // Example with a missing curly brace. + invalidUseCase = outdent( + String.raw` + i18n( + :args='{ name: ourUsername' + ) Hello {name}!` + ) + + errors = linter.checkString(invalidUseCase) + + assertEquals(errors.length, 2) + assertEquals(errors[0].code, errorCode) + assertMatch(errors[0].message, /unexpected token/i) + + assertEquals(errors[1].code, errorCode) + assertMatch(errors[1].message, /undefined named argument 'name'/i) + + // Example with an extraneous semicolon. + invalidUseCase = outdent( + String.raw` + i18n( + :args='{ name: ourUsername };' + ) Hello {name}!` + ) + + errors = linter.checkString(invalidUseCase) + assertEquals(errors.length, 1) + + // Example with an invalid property key. + invalidUseCase = outdent( + String.raw` + i18n( + :args='{ 0name: ourUsername };' + ) Hello {name}!` + ) + + errors = linter.checkString(invalidUseCase) + assert(linter.checkString(invalidUseCase).length > 0, invalidUseCase) + }) + + await tests.step('should report undefined ltags', async function () { + const invalidUseCase = outdent( + String.raw` + i18n( + :args='{ count: 5 }' + ) Invite {strong_}{count} members{_strong} to the party!` + ) + const errors = linter.checkString(invalidUseCase) + + assertEquals(errors.length, 2) + assertEquals(errors[0].code, errorCode) + assertMatch(errors[0].message, /undefined named argument 'strong_'/i) + assertEquals(errors[1].code, errorCode) + assertMatch(errors[1].message, /undefined named argument '_strong'/i) + }) + + await tests.step('should report undefined named arguments', async function () { + const invalidUseCase = outdent( + String.raw` + i18n( + :args='{ ...LTags("strong") }' + ) Invite {strong_}{count} members{_strong} to the party!` + ) + const errors = linter.checkString(invalidUseCase) + + assertEquals(errors.length, 1) + assertEquals(errors[0].code, errorCode) + assertMatch(errors[0].message, /undefined named argument 'count'/i) + }) + + await tests.step('should report unused ltags', async function () { + const invalidUseCase = outdent( + String.raw` + i18n( + :args='{ ...LTags("strong") }' + ) Invite your friends to the party!` + ) + const errors = linter.checkString(invalidUseCase) + + assertEquals(errors.length, 2) + assertEquals(errors[0].code, errorCode) + assertMatch(errors[0].message, /unused named argument 'strong_'/i) + assertEquals(errors[1].code, errorCode) + assertMatch(errors[1].message, /unused named argument '_strong'/i) + }) + + await tests.step('should report unused named arguments', async function () { + const invalidUseCase = outdent( + String.raw` + i18n( + :args='{ age, name }' + ) Hello {name}!` + ) + const errors = linter.checkString(invalidUseCase) + + assertEquals(errors.length, 1) + assertEquals(errors[0].code, errorCode) + assertMatch(errors[0].message, /unused named argument 'age'/i) + }) + + await tests.step('should report usage of the `html` attribute', async function () { + const invalidUseCase = outdent( + String.raw` + i18n( + tag='p' + html='My great text' + ) Hello` + ) + + const errors = linter.checkString(invalidUseCase) + assertEquals(errors.length, 1) + assertEquals(errors[0].code, errorCode) + assertMatch(errors[0].message, /html attribute/) + }) + + await tests.step('should report usage of double curly braces', async function () { + const invalidUseCase = 'i18n Replying to {{replyingTo}}' + const errors = linter.checkString(invalidUseCase) + + assertEquals(errors.length, 1) + assertEquals(errors[0].code, errorCode) + assertMatch(errors[0].message, /double curly braces/i) + }) + } +}) From bd7a1a162f213f6c8f1407f582328b508d2ba9f3 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Mon, 17 Oct 2022 20:32:48 +0200 Subject: [PATCH 27/43] Migrate disallow-vhtml-directive tests to Deno --- test/disallow-vhtml-directive.test.js | 50 ------------------------- test/disallow-vhtml-directive.test.ts | 54 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 50 deletions(-) delete mode 100644 test/disallow-vhtml-directive.test.js create mode 100644 test/disallow-vhtml-directive.test.ts diff --git a/test/disallow-vhtml-directive.test.js b/test/disallow-vhtml-directive.test.js deleted file mode 100644 index c6df2ab4a1..0000000000 --- a/test/disallow-vhtml-directive.test.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict' - -const assert = require('assert') - -const PugLinter = require('pug-lint') - -const linter = new PugLinter() - -linter.configure( - { - additionalRules: ['scripts/disallow-vhtml-directive.js'], - disallowVHTMLDirective: true - } -) - -const errorCode = 'PUG:LINT_DISALLOWVHTMLDIRECTIVE' - -function outdent (str) { - const lines = str.slice(1).split('\n') - const indent = (lines[0].match(/^\s*/) || [''])[0] - - if (indent === '') { - return lines.join('\n') - } - return lines.map( - line => line.startsWith(indent) ? line.slice(indent.length) : line - ).join('\n') -} - -it('should allow usage of the `v-safe-html` directive', function () { - const validUseCase = outdent( - String.raw` - p.p-description - span.has-text-1(v-safe-html='introTitle')` - ) - - assert.equal(linter.checkString(validUseCase).length, 0, validUseCase) -}) - -it('should disallow any usage of the `v-html` directive', function () { - const invalidUseCase = outdent( - String.raw` - p.p-description - span.has-text-1(v-html='introTitle')` - ) - - const errors = linter.checkString(invalidUseCase) - assert.equal(errors.length, 1) - assert.equal(errors[0].code, errorCode) -}) diff --git a/test/disallow-vhtml-directive.test.ts b/test/disallow-vhtml-directive.test.ts new file mode 100644 index 0000000000..fbf8c44f14 --- /dev/null +++ b/test/disallow-vhtml-directive.test.ts @@ -0,0 +1,54 @@ +import { assertEquals } from 'asserts' + +import { createRequire } from "https://deno.land/std/node/module.ts"; +import PugLinter from 'pug-lint' + +// HACK for 'dynamic require is not supported' error in 'linter.configure()'. +globalThis.require = createRequire(import.meta.url); + +Deno.test({ + name: 'Tests for the disallow-vhtml-directive linter rule', + fn: async function (tests) { + const errorCode = 'PUG:LINT_DISALLOWVHTMLDIRECTIVE' + const linter = new PugLinter() + + linter.configure( + { + additionalRules: ['scripts/disallow-vhtml-directive.js'], + disallowVHTMLDirective: true + } + ) + const outdent = (str) => { + const lines = str.slice(1).split('\n') + const indent = (lines[0].match(/^\s*/) || [''])[0] + + if (indent === '') { + return lines.join('\n') + } + return lines.map( + line => line.startsWith(indent) ? line.slice(indent.length) : line + ).join('\n') + } + + await tests.step('should allow usage of the `v-safe-html` directive', async function () { + const validUseCase = outdent( + String.raw` + p.p-description + span.has-text-1(v-safe-html='introTitle')` + ) + + assertEquals(linter.checkString(validUseCase).length, 0, validUseCase) + }) + + await tests.step('should disallow any usage of the `v-html` directive', async function () { + const invalidUseCase = outdent( + String.raw` + p.p-description + span.has-text-1(v-html='introTitle')` + ) + const errors = linter.checkString(invalidUseCase) + assertEquals(errors.length, 1) + assertEquals(errors[0].code, errorCode) + }) + } +}) From 3e7ca712b94265e7dfe04e4b6b00cf6eded7dc45 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Tue, 18 Oct 2022 09:49:08 +0200 Subject: [PATCH 28/43] Fix lint errors --- Gruntfile.js | 2 +- backend/database.ts | 2 +- backend/index.ts | 2 +- scripts/process-shim.ts | 1 + test/.eslintrc.json | 23 +++++++++++++++++++++++ test/avatar-caching.test.ts | 8 ++------ test/backend.test.ts | 15 ++++++--------- test/cypress/support/output-logs.js | 1 + test/disallow-vhtml-directive.test.ts | 4 ++-- test/validate-i18n.test.ts | 6 +++--- 10 files changed, 41 insertions(+), 23 deletions(-) create mode 100644 test/.eslintrc.json diff --git a/Gruntfile.js b/Gruntfile.js index d8c93f5745..07b94b5e72 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -391,7 +391,7 @@ module.exports = (grunt) => { exec: { chelDeployAll: 'find contracts -iname "*.manifest.json" | xargs -r ./node_modules/.bin/chel deploy ./data', - eslint: 'node ./node_modules/eslint/bin/eslint.js --cache "**/*.{js,vue}"', + eslint: 'node ./node_modules/eslint/bin/eslint.js --cache "**/*.{js,ts,vue}"', flow: '"./node_modules/.bin/flow" --quiet || echo The Flow check failed!', puglint: '"./node_modules/.bin/pug-lint-vue" frontend/views', stylelint: 'node ./node_modules/stylelint/bin/stylelint.js --cache "frontend/assets/style/**/*.{css,sass,scss}" "frontend/views/**/*.vue"', diff --git a/backend/database.ts b/backend/database.ts index c5552a871e..de9e427010 100644 --- a/backend/database.ts +++ b/backend/database.ts @@ -226,7 +226,7 @@ if (production || Deno.env.get('GI_PERSIST')) { cache.set(filename, value) return value }, - 'chelonia/db/set': async function (filename: string, data: any): Promise { + 'chelonia/db/set': async function (filename: string, data: unknown): Promise { // eslint-disable-next-line no-useless-catch try { const result = await sbp('backend/db/writeFile', filename, data) diff --git a/backend/index.ts b/backend/index.ts index 0ce2c2d4c0..8edcc0f6bd 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -8,7 +8,7 @@ import { notFound } from 'pogo/lib/bang.ts' import '~/scripts/process-shim.ts' import { SERVER_RUNNING } from './events.ts' import { PUBSUB_INSTANCE } from './instance-keys.ts' -import type { PubsubClient, PubsubServer } from './pubsub.ts' +import type { PubsubServer } from './pubsub.ts' // @ts-expect-error TS7017 [ERROR]: Element implicitly has an 'any' type. globalThis.logger = function (err: Error) { diff --git a/scripts/process-shim.ts b/scripts/process-shim.ts index 697d5439fd..2757ad3452 100644 --- a/scripts/process-shim.ts +++ b/scripts/process-shim.ts @@ -1,3 +1,4 @@ +/* globals Deno */ const process = { env: { get (key: string): string | void { diff --git a/test/.eslintrc.json b/test/.eslintrc.json new file mode 100644 index 0000000000..df53c9b133 --- /dev/null +++ b/test/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "parserOptions": { + "parser": "@typescript-eslint/parser" + }, + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "plugins": [ + "@typescript-eslint", + "import" + ], + "rules": { + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "dot-notation": "off", + "import/extensions": [ + 2, + "ignorePackages" + ], + "no-use-before-define": "off", + "quote-props": "off", + "require-await": "off" + } +} diff --git a/test/avatar-caching.test.ts b/test/avatar-caching.test.ts index 77a4e4be8e..f37842b2ee 100644 --- a/test/avatar-caching.test.ts +++ b/test/avatar-caching.test.ts @@ -3,17 +3,13 @@ import { assertEquals, assertMatch, assertNotMatch -} from "https://deno.land/std@0.153.0/testing/asserts.ts" - -import { bold, red, yellow } from 'fmt/colors.ts' -import * as pathlib from 'path' +} from 'https://deno.land/std@0.153.0/testing/asserts.ts' import '~/scripts/process-shim.ts' Deno.test({ name: 'Avatar file serving', fn: async function (tests) { - const apiURL = process.env.API_URL ?? 'http://localhost:8000' const hash = '21XWnNX5exusmJoJNWNNqjhWPqxGURryWbkUhYVsGT5NFtSGKs' @@ -33,5 +29,5 @@ Deno.test({ }) }, sanitizeResources: false, - sanitizeOps: false, + sanitizeOps: false }) diff --git a/test/backend.test.ts b/test/backend.test.ts index 4df0cea5c9..8426c6aec4 100644 --- a/test/backend.test.ts +++ b/test/backend.test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertMatch } from "https://deno.land/std@0.153.0/testing/asserts.ts" +import { assertEquals, assertMatch } from 'https://deno.land/std@0.153.0/testing/asserts.ts' import { bold, red, yellow } from 'fmt/colors.ts' import * as pathlib from 'path' @@ -19,7 +19,7 @@ import { INVITE_INITIAL_CREATOR, INVITE_EXPIRES_IN_DAYS, MAIL_TYPE_MESSAGE, PROP import { createInvite } from './contracts/shared/functions.js' import '~/frontend/controller/namespace.js' import { THEME_LIGHT } from '~/frontend/utils/themes.js' -import manifests from '~/frontend/model/contracts/manifests.json' assert { type: "json" } +import manifests from '~/frontend/model/contracts/manifests.json' assert { type: 'json' } import packageJSON from '~/package.json' assert { type: 'json' } const { version } = packageJSON @@ -59,9 +59,6 @@ Object.assign(process.env, applyPortShift(process.env)) Deno.env.set('GI_VERSION', `${version}@${new Date().toISOString()}`) -const API_PORT = Deno.env.get('API_PORT') -const API_URL = Deno.env.get('API_URL') -const CI = Deno.env.get('CI') const GI_VERSION = Deno.env.get('GI_VERSION') const NODE_ENV = Deno.env.get('NODE_ENV') ?? 'development' @@ -113,8 +110,8 @@ Deno.test({ const groups = {} // Wait for the server to be ready. - let t0 = Date.now() - let timeout = 30000 + const t0 = Date.now() + const timeout = 30000 await new Promise((resolve, reject) => { (function ping () { console.log(process.env.API_URL) @@ -182,7 +179,7 @@ Deno.test({ }) return msg } - function createGroup (name: string, hooks: Object = {}): Promise { + function createGroup (name: string, hooks: Record = {}): Promise { const initialInvite = createInvite({ quantity: 60, creator: INVITE_INITIAL_CREATOR, @@ -415,7 +412,7 @@ Deno.test({ }) }, sanitizeResources: false, - sanitizeOps: false, + sanitizeOps: false }) // Potentially useful for dealing with fetch API: diff --git a/test/cypress/support/output-logs.js b/test/cypress/support/output-logs.js index 05fcb86f97..7dd1b222f3 100644 --- a/test/cypress/support/output-logs.js +++ b/test/cypress/support/output-logs.js @@ -3,6 +3,7 @@ // Copied directly from: https://github.com/cypress-io/cypress/issues/3199#issuecomment-466593084 // *********** +// eslint-disable-next-line @typescript-eslint/no-var-requires const APPLICATION_NAME = require('../../../package.json').name let logs = '' diff --git a/test/disallow-vhtml-directive.test.ts b/test/disallow-vhtml-directive.test.ts index fbf8c44f14..2328612a1b 100644 --- a/test/disallow-vhtml-directive.test.ts +++ b/test/disallow-vhtml-directive.test.ts @@ -1,10 +1,10 @@ import { assertEquals } from 'asserts' -import { createRequire } from "https://deno.land/std/node/module.ts"; +import { createRequire } from 'https://deno.land/std/node/module.ts' import PugLinter from 'pug-lint' // HACK for 'dynamic require is not supported' error in 'linter.configure()'. -globalThis.require = createRequire(import.meta.url); +globalThis.require = createRequire(import.meta.url) Deno.test({ name: 'Tests for the disallow-vhtml-directive linter rule', diff --git a/test/validate-i18n.test.ts b/test/validate-i18n.test.ts index 53e978ef15..70a52cade0 100644 --- a/test/validate-i18n.test.ts +++ b/test/validate-i18n.test.ts @@ -1,14 +1,14 @@ import { assert, assertEquals, - assertMatch, + assertMatch } from 'asserts' -import { createRequire } from "https://deno.land/std/node/module.ts"; +import { createRequire } from 'https://deno.land/std/node/module.ts' import PugLinter from 'pug-lint' // HACK for 'dynamic require is not supported' error in 'linter.configure()'. -globalThis.require = createRequire(import.meta.url); +globalThis.require = createRequire(import.meta.url) Deno.test({ name: 'i18n tag validation', From c6e2601f51d3143d604532f2a4957fce407ec067 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Thu, 20 Oct 2022 13:03:26 +0200 Subject: [PATCH 29/43] Restore Flow typings of a few more files --- .flowconfig | 15 ++--- frontend/controller/namespace.js | 14 ++-- frontend/controller/utils/misc.js | 13 ++-- frontend/model/contracts/group.js | 5 +- frontend/model/contracts/manifests.json | 2 +- frontend/model/contracts/shared/giLodash.js | 66 ++++++++++++------- .../contracts/shared/voting/proposals.js | 5 +- shared/declarations.js | 16 +---- test/common/common.js.map | 4 +- test/contracts/chatroom.js.map | 4 +- test/contracts/group.js | 5 +- test/contracts/group.js.map | 4 +- test/contracts/identity.js.map | 4 +- test/contracts/mailbox.js.map | 4 +- test/contracts/shared/functions.js.map | 4 +- test/contracts/shared/giLodash.js | 15 +++++ test/contracts/shared/giLodash.js.map | 4 +- test/contracts/shared/voting/proposals.js.map | 4 +- 18 files changed, 115 insertions(+), 73 deletions(-) diff --git a/.flowconfig b/.flowconfig index 636735a325..f3fc913c4f 100644 --- a/.flowconfig +++ b/.flowconfig @@ -8,13 +8,11 @@ # - https://flowtype.org/docs/functions.html .*/Gruntfile.js # Backend files use TypeScript instead of Flow since they run using Deno. -.*/backend/.* +/backend/.* +/contracts/.* # Shared files must be imported by Deno backend files, so they better not contain Flowtype annotations. -.*/shared/.* -!.*/shared/types.flow.js -!.*/shared/.*/types.flow.js +/shared/.* .*/dist/.* -.*/contracts/.* .*/frontend/assets/.* .*/frontend/controller/service-worker.js .*/frontend/utils/blockies.js @@ -25,13 +23,10 @@ .*/ignored/.* .*/node_modules/.* .*/scripts/.* -.*/test/backend.js .*/test/.* -.*/test/frontend.js .*.test.js -# Not using Flow because backend tests are importing it. -.*/frontend/controller/namespace.js -.*/frontend/controller/utils/misc.js +# Don't ignore declaration files. +!.*/types.flow.js [libs] ./shared/declarations.js diff --git a/frontend/controller/namespace.js b/frontend/controller/namespace.js index a6bd1b5d67..e612606d18 100644 --- a/frontend/controller/namespace.js +++ b/frontend/controller/namespace.js @@ -1,4 +1,3 @@ -// @no-flow 'use strict' import sbp from '@sbp/sbp' @@ -7,7 +6,11 @@ import { handleFetchResult } from './utils/misc.js' // NOTE: prefix groups with `group/` and users with `user/` ? sbp('sbp/selectors/register', { - 'namespace/register': (name, value) => { + /* + * @param {string} name + * @param {string} value + */ + 'namespace/register': (name /*: string */, value /*: string */) => { return fetch(`${sbp('okTurtles.data/get', 'API_URL')}/name`, { method: 'POST', body: JSON.stringify({ name, value }), @@ -19,7 +22,10 @@ sbp('sbp/selectors/register', { return result }) }, - 'namespace/lookup': (name) => { + /* + * @param {string} name + */ + 'namespace/lookup': (name /*: string */) => { // TODO: should `name` be encodeURI'd? const cache = sbp('state/vuex/state').namespaceLookups if (name in cache) { @@ -33,7 +39,7 @@ sbp('sbp/selectors/register', { } return null } - return r['text']() + return r.text() }).then(value => { if (value !== null) { Vue.set(cache, name, value) diff --git a/frontend/controller/utils/misc.js b/frontend/controller/utils/misc.js index 6ebddc712c..61e21cf4ef 100644 --- a/frontend/controller/utils/misc.js +++ b/frontend/controller/utils/misc.js @@ -1,9 +1,14 @@ -// @noflow 'use strict' -export function handleFetchResult (type) { - return function (r) { +export function handleFetchResult (type /*: string */) /*: (r: Response) => any */ { + return function (r /*: Response */) /*: any */ { if (!r.ok) throw new Error(`${r.status}: ${r.statusText}`) - return r[type]() + // Can't just write `r[type]` here because of a Flow error. + switch (type) { + case 'blob': return r.blob() + case 'json': return r.json() + case 'text': return r.text() + default: throw new TypeError(`Invalid fetch result type: ${type}.`) + } } } diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index d710ac7a42..b43638d1cf 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -127,7 +127,10 @@ function isActionYoungerThanUser (actionMeta: Object, userProfile: ?Object): boo // 'MEMBER_ADDED' notification for user-1. // In some situations, userProfile is undefined, for example, when inviteAccept is called in // certain situations. So we need to check for that here. - return Boolean(userProfile) && compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0 + if (!userProfile) { + return false + } + return compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0 } sbp('chelonia/defineContract', { diff --git a/frontend/model/contracts/manifests.json b/frontend/model/contracts/manifests.json index ca27964714..ce49271537 100644 --- a/frontend/model/contracts/manifests.json +++ b/frontend/model/contracts/manifests.json @@ -1,7 +1,7 @@ { "manifests": { "gi.contracts/chatroom": "21XWnNKagK1f8W8FSbjdQQtBCpXC8XtkH2bBLPuLKSWDWVV4wt", - "gi.contracts/group": "21XWnNFhvfTnBzBN3Q17fsSSFqt7bq1DdDUv7XV18qkph2Ffye", + "gi.contracts/group": "21XWnNNca6WMqTeFNoA6wfyySfjfzfqZma7Az5hBhPaZwp4fnH", "gi.contracts/identity": "21XWnNWkuQ9chJiYM9QRhy2ULViMgB5UsMAzV7LsYNM3C4PUkW", "gi.contracts/mailbox": "21XWnNFVAWNh58rrzw4QT1oSS6zuLMnxkjnXMCPYmyHEUPcuZ4" } diff --git a/frontend/model/contracts/shared/giLodash.js b/frontend/model/contracts/shared/giLodash.js index e09676bc68..805972e793 100644 --- a/frontend/model/contracts/shared/giLodash.js +++ b/frontend/model/contracts/shared/giLodash.js @@ -2,22 +2,22 @@ // https://github.com/lodash/babel-plugin-lodash // additional tiny versions of lodash functions are available in VueScript2 -export function mapValues (obj, fn, o = {}) { +export function mapValues (obj /*: Object */, fn /*: Function */, o /*: Object */ = {}) /*: any */ { for (const key in obj) { o[key] = fn(obj[key]) } return o } -export function mapObject (obj, fn) { +export function mapObject (obj /*: Object */, fn /*: Function */) /*: {[any]: any} */ { return Object.fromEntries(Object.entries(obj).map(fn)) } -export function pick (o, props) { +export function pick (o /*: Object */, props /*: string[] */) /*: Object */ { const x = {} for (const k of props) { x[k] = o[k] } return x } -export function pickWhere (o, where) { +export function pickWhere (o /*: Object */, where /*: Function */) /*: Object */ { const x = {} for (const k in o) { if (where(o[k])) { x[k] = o[k] } @@ -25,13 +25,13 @@ export function pickWhere (o, where) { return x } -export function choose (array, indices) { +export function choose (array /*: Array<*> */, indices /*: Array */) /*: Array<*> */ { const x = [] for (const idx of indices) { x.push(array[idx]) } return x } -export function omit (o, props) { +export function omit (o /*: Object */, props /*: string[] */) /*: {...} */ { const x = {} for (const k in o) { if (!props.includes(k)) { @@ -41,7 +41,7 @@ export function omit (o, props) { return x } -export function cloneDeep (obj) { +export function cloneDeep (obj /*: Object */) /*: any */ { return JSON.parse(JSON.stringify(obj)) } @@ -51,7 +51,7 @@ function isMergeableObject (val) { return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]' } -export function merge (obj, src) { +export function merge (obj /*: Object */, src /*: Object */) /*: any */ { for (const key in src) { const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined if (clone && isMergeableObject(obj[key])) { @@ -63,31 +63,31 @@ export function merge (obj, src) { return obj } -export function delay (msec) { +export function delay (msec /*: number */) /*: Promise */ { return new Promise((resolve, reject) => { setTimeout(resolve, msec) }) } -export function randomBytes (length) { +export function randomBytes (length /*: number */) /*: Uint8Array */ { // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019 return crypto.getRandomValues(new Uint8Array(length)) } -export function randomHexString (length) { +export function randomHexString (length /*: number */) /*: string */ { return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('') } -export function randomIntFromRange (min, max) { +export function randomIntFromRange (min /*: number */, max /*: number */) /*: number */ { return Math.floor(Math.random() * (max - min + 1) + min) } -export function randomFromArray (arr) { +export function randomFromArray (arr /*: any[] */) /*: any */ { return arr[Math.floor(Math.random() * arr.length)] } -export function flatten (arr) { - let flat = [] +export function flatten (arr /*: Array<*> */) /*: Array */ { + let flat /*: Array<*> */ = [] for (let i = 0; i < arr.length; i++) { if (Array.isArray(arr[i])) { flat = flat.concat(arr[i]) @@ -98,7 +98,7 @@ export function flatten (arr) { return flat } -export function zip () { +export function zip () /*: any[] */ { // $FlowFixMe const arr = Array.prototype.slice.call(arguments) const zipped = [] @@ -113,26 +113,26 @@ export function zip () { return zipped } -export function uniq (array) { +export function uniq (array /*: any[] */) /*: any[] */ { return Array.from(new Set(array)) } -export function union (...arrays) { +export function union (...arrays /*: any[][] */) /*: any[] */ { // $FlowFixMe return uniq([].concat.apply([], arrays)) } -export function intersection (a1, ...arrays) { +export function intersection (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ { return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0)) } -export function difference (a1, ...arrays) { +export function difference (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ { // $FlowFixMe const a2 = [].concat.apply([], arrays) return a1.filter(v => a2.indexOf(v) === -1) } -export function deepEqualJSONType (a, b) { +export function deepEqualJSONType (a /*: any */, b /*: any */) /*: boolean */ { if (a === b) return true if (a === null || b === null || typeof (a) !== typeof (b)) return false if (typeof a !== 'object') return a === b @@ -162,7 +162,7 @@ export function deepEqualJSONType (a, b) { * @param {Boolean} whether to execute at the beginning (`false`) * @api public */ -export function debounce (func, wait, immediate) { +export function debounce (func /*: Function */, wait /*: number */, immediate /*: ?boolean */) /*: Function */ { let timeout, args, context, timestamp, result if (wait == null) wait = 100 @@ -212,3 +212,25 @@ export function debounce (func, wait, immediate) { return debounced } + +/** + * Gets the value at `path` of `obj`. If the resolved value is + * `undefined`, the `defaultValue` is returned in its place. + * + */ +export function get (obj /*: Object */, path /*: string[] */, defaultValue /*: any */) /*: any */ { + if (!path.length) { + return obj + } else if (obj === undefined) { + return defaultValue + } + + let result = obj + let i = 0 + while (result && i < path.length) { + result = result[path[i]] + i++ + } + + return result === undefined ? defaultValue : result +} diff --git a/frontend/model/contracts/shared/voting/proposals.js b/frontend/model/contracts/shared/voting/proposals.js index 166fb8eb5f..0922fdc451 100644 --- a/frontend/model/contracts/shared/voting/proposals.js +++ b/frontend/model/contracts/shared/voting/proposals.js @@ -19,7 +19,10 @@ import { // STATUS_CANCELLED } from '../constants.js' -export function archiveProposal ({ state, proposalHash, proposal, contractID }) { +export function archiveProposal ( + { state, proposalHash, proposal, contractID }: { + state: Object, proposalHash: string, proposal: any, contractID: string + }) { Vue.delete(state.proposals, proposalHash) sbp('gi.contracts/group/pushSideEffect', contractID, ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal] diff --git a/shared/declarations.js b/shared/declarations.js index eba6defc9e..1d7acf1cc7 100644 --- a/shared/declarations.js +++ b/shared/declarations.js @@ -62,11 +62,10 @@ declare module 'lru-cache' { declare module.exports: any } // Only necessary because `AppStyles.vue` imports it from its script tag rather than its style tag. declare module '@assets/style/main.scss' { declare module.exports: any } // Other .js files. +declare module '@common/common.js' { declare module.exports: any } declare module '@utils/blockies.js' { declare module.exports: Object } -declare module '~/frontend/controller/utils/misc.js' { declare module.exports: Function } declare module '~/frontend/model/contracts/misc/flowTyper.js' { declare module.exports: Object } declare module '~/frontend/model/contracts/shared/time.js' { declare module.exports: Object } -declare module '@model/contracts/shared/time.js' { declare module.exports: Object } // HACK: declared some shared files below but not sure why it's necessary declare module '~/shared/domains/chelonia/chelonia.ts' { declare module.exports: any } declare module '~/shared/domains/chelonia/errors.ts' { declare module.exports: Object } @@ -74,15 +73,6 @@ declare module '~/shared/domains/chelonia/events.ts' { declare module.exports: O declare module '~/shared/domains/chelonia/internals.ts' { declare module.exports: Object } declare module '~/shared/functions.ts' { declare module.exports: any } declare module '~/shared/pubsub.ts' { declare module.exports: any } - -declare module '~/frontend/model/contracts/shared/giLodash.js' { declare module.exports: any } -declare module '@model/contracts/shared/giLodash.js' { declare module.exports: any } -declare module '@model/contracts/shared/constants.js' { declare module.exports: any } -declare module '@model/contracts/shared/distribution/distribution.js' { declare module.exports: any } -declare module '@model/contracts/shared/voting/rules.js' { declare module.exports: any } -declare module '@model/contracts/shared/voting/proposals.js' { declare module.exports: any } -declare module '@model/contracts/shared/functions.js' { declare module.exports: any } -declare module '@common/common.js' { declare module.exports: any } -declare module './controller/namespace.js' { declare module.exports: any } +// JSON files. declare module './model/contracts/manifests.json' { declare module.exports: any } -declare module './utils/misc.js' { declare module.exports: any } + diff --git a/test/common/common.js.map b/test/common/common.js.map index f74b53c7bc..f731d0a7f0 100644 --- a/test/common/common.js.map +++ b/test/common/common.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../frontend/common/common.js", "../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/common/errors.js"], - "sourcesContent": ["'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nexport class GIErrorIgnoreAndBan extends Error {\n // ugly boilerplate because JavaScript is stupid\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types\n constructor (...params ) {\n super(...params)\n this.name = 'GIErrorIgnoreAndBan'\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n\n// Used to throw human readable errors on UI.\nexport class GIErrorUIRuntimeError extends Error {\n constructor (...params ) {\n super(...params)\n // this.name = this.constructor.name\n this.name = 'GIErrorUIRuntimeError' // string literal so minifier doesn't overwrite\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n"], - "mappings": "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", + "sourcesContent": ["'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj /*: Object */, fn /*: Function */, o /*: Object */ = {}) /*: any */ {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj /*: Object */, fn /*: Function */) /*: {[any]: any} */ {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o /*: Object */, props /*: string[] */) /*: Object */ {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o /*: Object */, where /*: Function */) /*: Object */ {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array /*: Array<*> */, indices /*: Array */) /*: Array<*> */ {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o /*: Object */, props /*: string[] */) /*: {...} */ {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj /*: Object */) /*: any */ {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj /*: Object */, src /*: Object */) /*: any */ {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec /*: number */) /*: Promise */ {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length /*: number */) /*: Uint8Array */ {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length /*: number */) /*: string */ {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min /*: number */, max /*: number */) /*: number */ {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr /*: any[] */) /*: any */ {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr /*: Array<*> */) /*: Array */ {\n let flat /*: Array<*> */ = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () /*: any[] */ {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array /*: any[] */) /*: any[] */ {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a /*: any */, b /*: any */) /*: boolean */ {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func /*: Function */, wait /*: number */, immediate /*: ?boolean */) /*: Function */ {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n\n/**\n * Gets the value at `path` of `obj`. If the resolved value is\n * `undefined`, the `defaultValue` is returned in its place.\n *\n */\nexport function get (obj /*: Object */, path /*: string[] */, defaultValue /*: any */) /*: any */ {\n if (!path.length) {\n return obj\n } else if (obj === undefined) {\n return defaultValue\n }\n\n let result = obj\n let i = 0\n while (result && i < path.length) {\n result = result[path[i]]\n i++\n }\n\n return result === undefined ? defaultValue : result\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nexport class GIErrorIgnoreAndBan extends Error {\n // ugly boilerplate because JavaScript is stupid\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types\n constructor (...params ) {\n super(...params)\n this.name = 'GIErrorIgnoreAndBan'\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n\n// Used to throw human readable errors on UI.\nexport class GIErrorUIRuntimeError extends Error {\n constructor (...params ) {\n super(...params)\n // this.name = this.constructor.name\n this.name = 'GIErrorUIRuntimeError' // string literal so minifier doesn't overwrite\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/chatroom.js.map b/test/contracts/chatroom.js.map index 4aa5c754d6..a6b20b1de7 100644 --- a/test/contracts/chatroom.js.map +++ b/test/contracts/chatroom.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/shared/time.js", "../../frontend/views/utils/misc.js", "../../frontend/model/contracts/shared/functions.js", "../../frontend/model/contracts/shared/nativeNotification.js", "../../frontend/model/contracts/chatroom.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nexport function logExceptNavigationDuplicated (err ) {\n err.name !== 'NavigationDuplicated' && console.error(err)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { INVITE_STATUS, MESSAGE_TYPES } from './constants.js'\nimport { DAYS_MILLIS } from './time.js'\nimport { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js'\n\n// !!!!!!!!!!!!!!!\n// !! IMPORTANT !!\n// !!!!!!!!!!!!!!!\n//\n// DO NOT CHANGE THE LOGIC TO ANY OF THESE FUNCTIONS!\n// INSTEAD, CREATE NEW FUNCTIONS WITH DIFFERENT NAMES\n// AND USE THOSE INSTEAD!\n//\n// THIS IS A CONSEQUENCE OF SHARING THIS CODE WITH THE REST OF THE APP.\n// IF YOU DO NOT NEED TO SHARE CODE WITH THE REST OF THE APP (AND CAN\n// KEEP IT WITHIN THE CONTRACT ONLY), THEN YOU DON'T NEED TO WORRY ABOUT\n// THIS, AND SHOULD INCLUDE THOSE FUNCTIONS (WITHOUT EXPORTING THEM),\n// DIRECTLY IN YOUR CONTRACT DEFINITION FILE. THEN YOU CAN MODIFY\n// THEM AS MUCH AS YOU LIKE (and generate new contract versions out of them).\n\n// group.js related\n\nexport function createInvite ({ quantity = 1, creator, expires, invitee } \n \n ) \n \n \n \n \n \n \n \n {\n return {\n inviteSecret: `${parseInt(Math.random() * 10000)}`, // TODO: this\n quantity,\n creator,\n invitee,\n status: INVITE_STATUS.VALID,\n responses: {}, // { bob: true } list of usernames that accepted the invite.\n expires: Date.now() + DAYS_MILLIS * expires\n }\n}\n\n// chatroom.js related\n\nexport function createMessage ({ meta, data, hash, state } \n \n ) {\n const { type, text, replyingMessage } = data\n const { createdDate } = meta\n\n let newMessage = {\n type,\n datetime: new Date(createdDate).toISOString(),\n id: hash,\n from: meta.username\n }\n\n if (type === MESSAGE_TYPES.TEXT) {\n newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }\n } else if (type === MESSAGE_TYPES.POLL) {\n // TODO: Poll message creation\n } else if (type === MESSAGE_TYPES.NOTIFICATION) {\n const params = {\n channelName: state?.attributes.name,\n channelDescription: state?.attributes.description,\n ...data.notification\n }\n delete params.type\n newMessage = {\n ...newMessage,\n notification: { type: data.notification.type, params }\n }\n } else if (type === MESSAGE_TYPES.INTERACTIVE) {\n // TODO: Interactive message creation for proposals\n }\n return newMessage\n}\n\nexport async function leaveChatRoom ({ contractID } \n \n ) {\n const rootState = sbp('state/vuex/state')\n const rootGetters = sbp('state/vuex/getters')\n if (contractID === rootGetters.currentChatRoomId) {\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {\n groupId: rootState.currentGroupId\n })\n const curRouteName = sbp('controller/router').history.current.name\n if (curRouteName === 'GroupChat' || curRouteName === 'GroupChatConversation') {\n await sbp('controller/router')\n .push({ name: 'GroupChatConversation', params: { chatRoomId: rootGetters.currentChatRoomId } })\n .catch(logExceptNavigationDuplicated)\n }\n }\n\n sbp('state/vuex/commit', 'deleteChatRoomUnread', { chatRoomId: contractID })\n sbp('state/vuex/commit', 'deleteChatRoomScrollPosition', { chatRoomId: contractID })\n\n // NOTE: make sure *not* to await on this, since that can cause\n // a potential deadlock. See same warning in sideEffect for\n // 'gi.contracts/group/removeMember'\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e)\n })\n}\n\nexport function findMessageIdx (id , messages ) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].id === id) {\n return i\n }\n }\n return -1\n}\n\nexport function makeMentionFromUsername (username ) \n \n {\n return {\n me: `@${username}`,\n all: '@all'\n }\n}\n", "'use strict'\nimport sbp from '@sbp/sbp'\n\n// NOTE: since these functions don't modify contract state, it should\n// be safe to modify them without worrying about version conflicts.\n\nexport async function requestNotificationPermission (force = false) {\n if (typeof Notification === 'undefined') {\n return null\n }\n if (force || Notification.permission === 'default') {\n try {\n sbp('state/vuex/commit', 'setNotificationEnabled', await Notification.requestPermission() === 'granted')\n } catch (e) {\n console.error('requestNotificationPermission:', e.message)\n return null\n }\n }\n return Notification.permission\n}\n\nexport function makeNotification ({ title, body, icon, path } \n \n ) {\n const notificationEnabled = sbp('state/vuex/state').notificationEnabled\n if (typeof Notification === 'undefined' || Notification.permission !== 'granted' || !notificationEnabled) {\n return\n }\n\n const notification = new Notification(title, { body, icon })\n if (path) {\n notification.onclick = function (event) {\n event.preventDefault()\n sbp('controller/router').push({ path }).catch(console.warn)\n }\n }\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, L } from '@common/common.js'\nimport { merge, cloneDeep } from './shared/giLodash.js'\nimport {\n CHATROOM_NAME_LIMITS_IN_CHARS,\n CHATROOM_DESCRIPTION_LIMITS_IN_CHARS,\n CHATROOM_ACTIONS_PER_PAGE,\n CHATROOM_MESSAGES_PER_PAGE,\n MESSAGE_TYPES,\n MESSAGE_NOTIFICATIONS,\n CHATROOM_MESSAGE_ACTION,\n MESSAGE_RECEIVE\n} from './shared/constants.js'\nimport { chatRoomAttributesType, messageType } from './shared/types.js'\nimport { createMessage, leaveChatRoom, findMessageIdx, makeMentionFromUsername } from './shared/functions.js'\nimport { makeNotification } from './shared/nativeNotification.js'\nimport { objectOf, string, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction createNotificationData (\n notificationType ,\n moreParams = {}\n) {\n return {\n type: MESSAGE_TYPES.NOTIFICATION,\n notification: {\n type: notificationType,\n ...moreParams\n }\n }\n}\n\nfunction emitMessageEvent ({ contractID, hash } \n \n \n ) {\n sbp('okTurtles.events/emit', `${CHATROOM_MESSAGE_ACTION}-${contractID}`, { hash })\n}\n\nfunction addMention ({ contractID, messageId, datetime, text, username, chatRoomName } \n \n \n \n \n \n \n ) {\n /**\n * If 'READY_TO_JOIN_CHATROOM' is false, it means not syncing chatroom\n */\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n return\n }\n\n sbp('state/vuex/commit', 'addChatRoomUnreadMention', {\n chatRoomId: contractID,\n messageId,\n createdDate: datetime\n })\n\n const rootGetters = sbp('state/vuex/getters')\n const groupID = rootGetters.groupIdFromChatRoomId(contractID)\n const path = `/group-chat/${contractID}`\n\n makeNotification({\n title: `# ${chatRoomName}`,\n body: text,\n icon: rootGetters.globalProfile2(groupID, username)?.picture,\n path\n })\n\n sbp('okTurtles.events/emit', MESSAGE_RECEIVE)\n}\n\nfunction deleteMention ({ contractID, messageId } \n \n ) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadMention', { chatRoomId: contractID, messageId })\n}\n\nfunction updateUnreadPosition ({ contractID, hash, createdDate } \n \n ) {\n sbp('state/vuex/commit', 'setChatRoomUnreadSince', {\n chatRoomId: contractID,\n messageId: hash,\n createdDate\n })\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/chatroom',\n metadata: {\n validate: objectOf({\n createdDate: string, // action created date\n username: string, // action creator\n identityContractID: string // action creator identityContractID\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n getters: {\n currentChatRoomState (state) {\n return state\n },\n chatRoomSettings (state, getters) {\n return getters.currentChatRoomState.settings || {}\n },\n chatRoomAttributes (state, getters) {\n return getters.currentChatRoomState.attributes || {}\n },\n chatRoomUsers (state, getters) {\n return getters.currentChatRoomState.users || {}\n },\n chatRoomLatestMessages (state, getters) {\n return getters.currentChatRoomState.messages || []\n }\n },\n actions: {\n // This is the constructor of Chat contract\n 'gi.contracts/chatroom': {\n validate: objectOf({\n attributes: chatRoomAttributesType\n }),\n process ({ meta, data }, { state }) {\n const initialState = merge({\n settings: {\n actionsPerPage: CHATROOM_ACTIONS_PER_PAGE,\n messagesPerPage: CHATROOM_MESSAGES_PER_PAGE,\n maxNameLength: CHATROOM_NAME_LIMITS_IN_CHARS,\n maxDescriptionLength: CHATROOM_DESCRIPTION_LIMITS_IN_CHARS\n },\n attributes: {\n creator: meta.username,\n deletedDate: null,\n archivedDate: null\n },\n users: {},\n messages: []\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/chatroom/join': {\n validate: objectOf({\n username: string // username of joining member\n }),\n process ({ data, meta, hash }, { state }) {\n const { username } = data\n if (!state.saveMessage && state.users[username]) {\n // this can happen when we're logging in on another machine, and also in other circumstances\n console.warn('Can not join the chatroom which you are already part of')\n return\n }\n\n Vue.set(state.users, username, { joinedDate: meta.createdDate })\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = username === meta.username ? MESSAGE_NOTIFICATIONS.JOIN_MEMBER : MESSAGE_NOTIFICATIONS.ADD_MEMBER\n const notificationData = createNotificationData(\n notificationType,\n notificationType === MESSAGE_NOTIFICATIONS.ADD_MEMBER ? { username } : {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM') || // Join by himself or Login in another device\n sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID') === contractID) { // Be added by another\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/rename': {\n validate: objectOf({\n name: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'name', data.name)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_NAME, {})\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/changeDescription': {\n validate: objectOf({\n description: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'description', data.description)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(\n MESSAGE_NOTIFICATIONS.UPDATE_DESCRIPTION, {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/leave': {\n validate: objectOf({\n username: optional(string), // coming from the gi.contracts/group/leaveChatRoom\n member: string // username to be removed\n }),\n process ({ data, meta, hash }, { state }) {\n const { member } = data\n const isKicked = data.username && member !== data.username\n if (!state.saveMessage && !state.users[member]) {\n throw new Error(`Can not leave the chatroom which ${member} are not part of`)\n }\n Vue.delete(state.users, member)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = !isKicked ? MESSAGE_NOTIFICATIONS.LEAVE_MEMBER : MESSAGE_NOTIFICATIONS.KICK_MEMBER\n const notificationData = createNotificationData(notificationType, isKicked ? { username: member } : {})\n const newMessage = createMessage({\n meta: isKicked ? meta : { ...meta, username: member },\n hash,\n data: notificationData,\n state\n })\n state.messages.push(newMessage)\n },\n sideEffect ({ data, hash, contractID, meta }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (data.member === rootState.loggedIn.username) {\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/delete': {\n validate: (data, { state, meta }) => {\n if (state.attributes.creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state, rootState }) {\n Vue.set(state.attributes, 'deletedDate', meta.createdDate)\n for (const username in state.users) {\n Vue.delete(state.users, username)\n }\n },\n sideEffect ({ meta, contractID }, { state }) {\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n },\n 'gi.contracts/chatroom/addMessage': {\n validate: messageType,\n process ({ data, meta, hash }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const pendingMsg = state.messages.find(msg => msg.id === hash && msg.pending)\n if (pendingMsg) {\n delete pendingMsg.pending\n } else {\n state.messages.push(createMessage({ meta, data, hash, state }))\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { state, getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const newMessage = createMessage({ meta, data, hash, state })\n const mentions = makeMentionFromUsername(me)\n if (data.type === MESSAGE_TYPES.TEXT &&\n (newMessage.text.includes(mentions.me) || newMessage.text.includes(mentions.all))) {\n addMention({\n contractID,\n messageId: newMessage.id,\n datetime: newMessage.datetime,\n text: newMessage.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n }\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/editMessage': {\n validate: (data, { state, meta }) => {\n objectOf({\n id: string,\n createdDate: string,\n text: string\n })(data)\n // TODO: Actually NOT SURE it's needed to check if the meta.username === message.from\n // there is no messagess in vuex state\n // to check if the meta.username is creator seems like too heavy\n },\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0 && meta.username === state.messages[msgIndex].from) {\n state.messages[msgIndex].text = data.text\n state.messages[msgIndex].updatedDate = meta.createdDate\n if (state.saveMessage && state.messages[msgIndex].pending) {\n delete state.messages[msgIndex].pending\n }\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const isAlreadyAdded = rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)\n const mentions = makeMentionFromUsername(me)\n const isIncludeMention = data.text.includes(mentions.me) || data.text.includes(mentions.all)\n if (!isAlreadyAdded && isIncludeMention) {\n addMention({\n contractID,\n messageId: data.id,\n /*\n * the following datetime is the time when the message(which made mention) is created\n * the reason why it is it instead of datetime when the mention created is because\n * it is compared to the datetime of other messages when user scrolls\n * to decide if it should be removed from the list of mentions or not\n */\n datetime: data.createdDate,\n text: data.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n } else if (isAlreadyAdded && !isIncludeMention) {\n deleteMention({ contractID, messageId: data.id })\n }\n }\n },\n 'gi.contracts/chatroom/deleteMessage': {\n validate: objectOf({\n id: string\n }),\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0) {\n state.messages.splice(msgIndex, 1)\n }\n // filter replied messages and check if the current message is original\n for (const message of state.messages) {\n if (message.replyingMessage?.id === data.id) {\n message.replyingMessage.id = null\n message.replyingMessage.text = L('Original message was removed by {username}', {\n username: makeMentionFromUsername(meta.username).me\n })\n }\n }\n },\n sideEffect ({ data, contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (rootState.chatRoomScrollPosition[contractID] === data.id) {\n sbp('state/vuex/commit', 'setChatRoomScrollPosition', {\n chatRoomId: contractID, messageId: null\n })\n }\n\n if (rootState.chatRoomUnread[contractID].since.messageId === data.id) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadSince', {\n chatRoomId: contractID,\n deletedDate: meta.createdDate\n })\n }\n\n if (me === meta.username) {\n return\n }\n if (rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)) {\n deleteMention({ contractID, messageId: data.id })\n }\n\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/makeEmotion': {\n validate: objectOf({\n id: string,\n emoticon: string\n }),\n process ({ data, meta, contractID }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const { id, emoticon } = data\n const msgIndex = findMessageIdx(id, state.messages)\n if (msgIndex >= 0) {\n let emoticons = cloneDeep(state.messages[msgIndex].emoticons || {})\n if (emoticons[emoticon]) {\n const alreadyAdded = emoticons[emoticon].indexOf(meta.username)\n if (alreadyAdded >= 0) {\n emoticons[emoticon].splice(alreadyAdded, 1)\n if (!emoticons[emoticon].length) {\n delete emoticons[emoticon]\n if (!Object.keys(emoticons).length) {\n emoticons = null\n }\n }\n } else {\n emoticons[emoticon].push(meta.username)\n }\n } else {\n emoticons[emoticon] = [meta.username]\n }\n if (emoticons) {\n Vue.set(state.messages[msgIndex], 'emoticons', emoticons)\n } else {\n Vue.delete(state.messages[msgIndex], 'emoticons')\n }\n }\n },\n sideEffect ({ contractID, hash }) {\n emitMessageEvent({ contractID, hash })\n }\n }\n }\n})\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj /*: Object */, fn /*: Function */, o /*: Object */ = {}) /*: any */ {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj /*: Object */, fn /*: Function */) /*: {[any]: any} */ {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o /*: Object */, props /*: string[] */) /*: Object */ {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o /*: Object */, where /*: Function */) /*: Object */ {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array /*: Array<*> */, indices /*: Array */) /*: Array<*> */ {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o /*: Object */, props /*: string[] */) /*: {...} */ {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj /*: Object */) /*: any */ {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj /*: Object */, src /*: Object */) /*: any */ {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec /*: number */) /*: Promise */ {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length /*: number */) /*: Uint8Array */ {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length /*: number */) /*: string */ {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min /*: number */, max /*: number */) /*: number */ {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr /*: any[] */) /*: any */ {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr /*: Array<*> */) /*: Array */ {\n let flat /*: Array<*> */ = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () /*: any[] */ {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array /*: any[] */) /*: any[] */ {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a /*: any */, b /*: any */) /*: boolean */ {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func /*: Function */, wait /*: number */, immediate /*: ?boolean */) /*: Function */ {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n\n/**\n * Gets the value at `path` of `obj`. If the resolved value is\n * `undefined`, the `defaultValue` is returned in its place.\n *\n */\nexport function get (obj /*: Object */, path /*: string[] */, defaultValue /*: any */) /*: any */ {\n if (!path.length) {\n return obj\n } else if (obj === undefined) {\n return defaultValue\n }\n\n let result = obj\n let i = 0\n while (result && i < path.length) {\n result = result[path[i]]\n i++\n }\n\n return result === undefined ? defaultValue : result\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nexport function logExceptNavigationDuplicated (err ) {\n err.name !== 'NavigationDuplicated' && console.error(err)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { INVITE_STATUS, MESSAGE_TYPES } from './constants.js'\nimport { DAYS_MILLIS } from './time.js'\nimport { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js'\n\n// !!!!!!!!!!!!!!!\n// !! IMPORTANT !!\n// !!!!!!!!!!!!!!!\n//\n// DO NOT CHANGE THE LOGIC TO ANY OF THESE FUNCTIONS!\n// INSTEAD, CREATE NEW FUNCTIONS WITH DIFFERENT NAMES\n// AND USE THOSE INSTEAD!\n//\n// THIS IS A CONSEQUENCE OF SHARING THIS CODE WITH THE REST OF THE APP.\n// IF YOU DO NOT NEED TO SHARE CODE WITH THE REST OF THE APP (AND CAN\n// KEEP IT WITHIN THE CONTRACT ONLY), THEN YOU DON'T NEED TO WORRY ABOUT\n// THIS, AND SHOULD INCLUDE THOSE FUNCTIONS (WITHOUT EXPORTING THEM),\n// DIRECTLY IN YOUR CONTRACT DEFINITION FILE. THEN YOU CAN MODIFY\n// THEM AS MUCH AS YOU LIKE (and generate new contract versions out of them).\n\n// group.js related\n\nexport function createInvite ({ quantity = 1, creator, expires, invitee } \n \n ) \n \n \n \n \n \n \n \n {\n return {\n inviteSecret: `${parseInt(Math.random() * 10000)}`, // TODO: this\n quantity,\n creator,\n invitee,\n status: INVITE_STATUS.VALID,\n responses: {}, // { bob: true } list of usernames that accepted the invite.\n expires: Date.now() + DAYS_MILLIS * expires\n }\n}\n\n// chatroom.js related\n\nexport function createMessage ({ meta, data, hash, state } \n \n ) {\n const { type, text, replyingMessage } = data\n const { createdDate } = meta\n\n let newMessage = {\n type,\n datetime: new Date(createdDate).toISOString(),\n id: hash,\n from: meta.username\n }\n\n if (type === MESSAGE_TYPES.TEXT) {\n newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }\n } else if (type === MESSAGE_TYPES.POLL) {\n // TODO: Poll message creation\n } else if (type === MESSAGE_TYPES.NOTIFICATION) {\n const params = {\n channelName: state?.attributes.name,\n channelDescription: state?.attributes.description,\n ...data.notification\n }\n delete params.type\n newMessage = {\n ...newMessage,\n notification: { type: data.notification.type, params }\n }\n } else if (type === MESSAGE_TYPES.INTERACTIVE) {\n // TODO: Interactive message creation for proposals\n }\n return newMessage\n}\n\nexport async function leaveChatRoom ({ contractID } \n \n ) {\n const rootState = sbp('state/vuex/state')\n const rootGetters = sbp('state/vuex/getters')\n if (contractID === rootGetters.currentChatRoomId) {\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {\n groupId: rootState.currentGroupId\n })\n const curRouteName = sbp('controller/router').history.current.name\n if (curRouteName === 'GroupChat' || curRouteName === 'GroupChatConversation') {\n await sbp('controller/router')\n .push({ name: 'GroupChatConversation', params: { chatRoomId: rootGetters.currentChatRoomId } })\n .catch(logExceptNavigationDuplicated)\n }\n }\n\n sbp('state/vuex/commit', 'deleteChatRoomUnread', { chatRoomId: contractID })\n sbp('state/vuex/commit', 'deleteChatRoomScrollPosition', { chatRoomId: contractID })\n\n // NOTE: make sure *not* to await on this, since that can cause\n // a potential deadlock. See same warning in sideEffect for\n // 'gi.contracts/group/removeMember'\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e)\n })\n}\n\nexport function findMessageIdx (id , messages ) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].id === id) {\n return i\n }\n }\n return -1\n}\n\nexport function makeMentionFromUsername (username ) \n \n {\n return {\n me: `@${username}`,\n all: '@all'\n }\n}\n", "'use strict'\nimport sbp from '@sbp/sbp'\n\n// NOTE: since these functions don't modify contract state, it should\n// be safe to modify them without worrying about version conflicts.\n\nexport async function requestNotificationPermission (force = false) {\n if (typeof Notification === 'undefined') {\n return null\n }\n if (force || Notification.permission === 'default') {\n try {\n sbp('state/vuex/commit', 'setNotificationEnabled', await Notification.requestPermission() === 'granted')\n } catch (e) {\n console.error('requestNotificationPermission:', e.message)\n return null\n }\n }\n return Notification.permission\n}\n\nexport function makeNotification ({ title, body, icon, path } \n \n ) {\n const notificationEnabled = sbp('state/vuex/state').notificationEnabled\n if (typeof Notification === 'undefined' || Notification.permission !== 'granted' || !notificationEnabled) {\n return\n }\n\n const notification = new Notification(title, { body, icon })\n if (path) {\n notification.onclick = function (event) {\n event.preventDefault()\n sbp('controller/router').push({ path }).catch(console.warn)\n }\n }\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, L } from '@common/common.js'\nimport { merge, cloneDeep } from './shared/giLodash.js'\nimport {\n CHATROOM_NAME_LIMITS_IN_CHARS,\n CHATROOM_DESCRIPTION_LIMITS_IN_CHARS,\n CHATROOM_ACTIONS_PER_PAGE,\n CHATROOM_MESSAGES_PER_PAGE,\n MESSAGE_TYPES,\n MESSAGE_NOTIFICATIONS,\n CHATROOM_MESSAGE_ACTION,\n MESSAGE_RECEIVE\n} from './shared/constants.js'\nimport { chatRoomAttributesType, messageType } from './shared/types.js'\nimport { createMessage, leaveChatRoom, findMessageIdx, makeMentionFromUsername } from './shared/functions.js'\nimport { makeNotification } from './shared/nativeNotification.js'\nimport { objectOf, string, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction createNotificationData (\n notificationType ,\n moreParams = {}\n) {\n return {\n type: MESSAGE_TYPES.NOTIFICATION,\n notification: {\n type: notificationType,\n ...moreParams\n }\n }\n}\n\nfunction emitMessageEvent ({ contractID, hash } \n \n \n ) {\n sbp('okTurtles.events/emit', `${CHATROOM_MESSAGE_ACTION}-${contractID}`, { hash })\n}\n\nfunction addMention ({ contractID, messageId, datetime, text, username, chatRoomName } \n \n \n \n \n \n \n ) {\n /**\n * If 'READY_TO_JOIN_CHATROOM' is false, it means not syncing chatroom\n */\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n return\n }\n\n sbp('state/vuex/commit', 'addChatRoomUnreadMention', {\n chatRoomId: contractID,\n messageId,\n createdDate: datetime\n })\n\n const rootGetters = sbp('state/vuex/getters')\n const groupID = rootGetters.groupIdFromChatRoomId(contractID)\n const path = `/group-chat/${contractID}`\n\n makeNotification({\n title: `# ${chatRoomName}`,\n body: text,\n icon: rootGetters.globalProfile2(groupID, username)?.picture,\n path\n })\n\n sbp('okTurtles.events/emit', MESSAGE_RECEIVE)\n}\n\nfunction deleteMention ({ contractID, messageId } \n \n ) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadMention', { chatRoomId: contractID, messageId })\n}\n\nfunction updateUnreadPosition ({ contractID, hash, createdDate } \n \n ) {\n sbp('state/vuex/commit', 'setChatRoomUnreadSince', {\n chatRoomId: contractID,\n messageId: hash,\n createdDate\n })\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/chatroom',\n metadata: {\n validate: objectOf({\n createdDate: string, // action created date\n username: string, // action creator\n identityContractID: string // action creator identityContractID\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n getters: {\n currentChatRoomState (state) {\n return state\n },\n chatRoomSettings (state, getters) {\n return getters.currentChatRoomState.settings || {}\n },\n chatRoomAttributes (state, getters) {\n return getters.currentChatRoomState.attributes || {}\n },\n chatRoomUsers (state, getters) {\n return getters.currentChatRoomState.users || {}\n },\n chatRoomLatestMessages (state, getters) {\n return getters.currentChatRoomState.messages || []\n }\n },\n actions: {\n // This is the constructor of Chat contract\n 'gi.contracts/chatroom': {\n validate: objectOf({\n attributes: chatRoomAttributesType\n }),\n process ({ meta, data }, { state }) {\n const initialState = merge({\n settings: {\n actionsPerPage: CHATROOM_ACTIONS_PER_PAGE,\n messagesPerPage: CHATROOM_MESSAGES_PER_PAGE,\n maxNameLength: CHATROOM_NAME_LIMITS_IN_CHARS,\n maxDescriptionLength: CHATROOM_DESCRIPTION_LIMITS_IN_CHARS\n },\n attributes: {\n creator: meta.username,\n deletedDate: null,\n archivedDate: null\n },\n users: {},\n messages: []\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/chatroom/join': {\n validate: objectOf({\n username: string // username of joining member\n }),\n process ({ data, meta, hash }, { state }) {\n const { username } = data\n if (!state.saveMessage && state.users[username]) {\n // this can happen when we're logging in on another machine, and also in other circumstances\n console.warn('Can not join the chatroom which you are already part of')\n return\n }\n\n Vue.set(state.users, username, { joinedDate: meta.createdDate })\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = username === meta.username ? MESSAGE_NOTIFICATIONS.JOIN_MEMBER : MESSAGE_NOTIFICATIONS.ADD_MEMBER\n const notificationData = createNotificationData(\n notificationType,\n notificationType === MESSAGE_NOTIFICATIONS.ADD_MEMBER ? { username } : {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM') || // Join by himself or Login in another device\n sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID') === contractID) { // Be added by another\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/rename': {\n validate: objectOf({\n name: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'name', data.name)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_NAME, {})\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/changeDescription': {\n validate: objectOf({\n description: string\n }),\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.attributes, 'description', data.description)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationData = createNotificationData(\n MESSAGE_NOTIFICATIONS.UPDATE_DESCRIPTION, {}\n )\n const newMessage = createMessage({ meta, hash, data: notificationData, state })\n state.messages.push(newMessage)\n },\n sideEffect ({ contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/leave': {\n validate: objectOf({\n username: optional(string), // coming from the gi.contracts/group/leaveChatRoom\n member: string // username to be removed\n }),\n process ({ data, meta, hash }, { state }) {\n const { member } = data\n const isKicked = data.username && member !== data.username\n if (!state.saveMessage && !state.users[member]) {\n throw new Error(`Can not leave the chatroom which ${member} are not part of`)\n }\n Vue.delete(state.users, member)\n\n if (!state.saveMessage) {\n return\n }\n\n const notificationType = !isKicked ? MESSAGE_NOTIFICATIONS.LEAVE_MEMBER : MESSAGE_NOTIFICATIONS.KICK_MEMBER\n const notificationData = createNotificationData(notificationType, isKicked ? { username: member } : {})\n const newMessage = createMessage({\n meta: isKicked ? meta : { ...meta, username: member },\n hash,\n data: notificationData,\n state\n })\n state.messages.push(newMessage)\n },\n sideEffect ({ data, hash, contractID, meta }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (data.member === rootState.loggedIn.username) {\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/delete': {\n validate: (data, { state, meta }) => {\n if (state.attributes.creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state, rootState }) {\n Vue.set(state.attributes, 'deletedDate', meta.createdDate)\n for (const username in state.users) {\n Vue.delete(state.users, username)\n }\n },\n sideEffect ({ meta, contractID }, { state }) {\n if (sbp('okTurtles.data/get', 'JOINING_CHATROOM_ID')) {\n return\n }\n leaveChatRoom({ contractID })\n }\n },\n 'gi.contracts/chatroom/addMessage': {\n validate: messageType,\n process ({ data, meta, hash }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const pendingMsg = state.messages.find(msg => msg.id === hash && msg.pending)\n if (pendingMsg) {\n delete pendingMsg.pending\n } else {\n state.messages.push(createMessage({ meta, data, hash, state }))\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { state, getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const newMessage = createMessage({ meta, data, hash, state })\n const mentions = makeMentionFromUsername(me)\n if (data.type === MESSAGE_TYPES.TEXT &&\n (newMessage.text.includes(mentions.me) || newMessage.text.includes(mentions.all))) {\n addMention({\n contractID,\n messageId: newMessage.id,\n datetime: newMessage.datetime,\n text: newMessage.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n }\n\n if (sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate })\n }\n }\n },\n 'gi.contracts/chatroom/editMessage': {\n validate: (data, { state, meta }) => {\n objectOf({\n id: string,\n createdDate: string,\n text: string\n })(data)\n // TODO: Actually NOT SURE it's needed to check if the meta.username === message.from\n // there is no messagess in vuex state\n // to check if the meta.username is creator seems like too heavy\n },\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0 && meta.username === state.messages[msgIndex].from) {\n state.messages[msgIndex].text = data.text\n state.messages[msgIndex].updatedDate = meta.createdDate\n if (state.saveMessage && state.messages[msgIndex].pending) {\n delete state.messages[msgIndex].pending\n }\n }\n },\n sideEffect ({ contractID, hash, meta, data }, { getters }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (me === meta.username) {\n return\n }\n const isAlreadyAdded = rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)\n const mentions = makeMentionFromUsername(me)\n const isIncludeMention = data.text.includes(mentions.me) || data.text.includes(mentions.all)\n if (!isAlreadyAdded && isIncludeMention) {\n addMention({\n contractID,\n messageId: data.id,\n /*\n * the following datetime is the time when the message(which made mention) is created\n * the reason why it is it instead of datetime when the mention created is because\n * it is compared to the datetime of other messages when user scrolls\n * to decide if it should be removed from the list of mentions or not\n */\n datetime: data.createdDate,\n text: data.text,\n username: meta.username,\n chatRoomName: getters.chatRoomAttributes.name\n })\n } else if (isAlreadyAdded && !isIncludeMention) {\n deleteMention({ contractID, messageId: data.id })\n }\n }\n },\n 'gi.contracts/chatroom/deleteMessage': {\n validate: objectOf({\n id: string\n }),\n process ({ data, meta }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const msgIndex = findMessageIdx(data.id, state.messages)\n if (msgIndex >= 0) {\n state.messages.splice(msgIndex, 1)\n }\n // filter replied messages and check if the current message is original\n for (const message of state.messages) {\n if (message.replyingMessage?.id === data.id) {\n message.replyingMessage.id = null\n message.replyingMessage.text = L('Original message was removed by {username}', {\n username: makeMentionFromUsername(meta.username).me\n })\n }\n }\n },\n sideEffect ({ data, contractID, hash, meta }) {\n emitMessageEvent({ contractID, hash })\n\n const rootState = sbp('state/vuex/state')\n const me = rootState.loggedIn.username\n\n if (rootState.chatRoomScrollPosition[contractID] === data.id) {\n sbp('state/vuex/commit', 'setChatRoomScrollPosition', {\n chatRoomId: contractID, messageId: null\n })\n }\n\n if (rootState.chatRoomUnread[contractID].since.messageId === data.id) {\n sbp('state/vuex/commit', 'deleteChatRoomUnreadSince', {\n chatRoomId: contractID,\n deletedDate: meta.createdDate\n })\n }\n\n if (me === meta.username) {\n return\n }\n if (rootState.chatRoomUnread[contractID].mentions.find(m => m.messageId === data.id)) {\n deleteMention({ contractID, messageId: data.id })\n }\n\n emitMessageEvent({ contractID, hash })\n }\n },\n 'gi.contracts/chatroom/makeEmotion': {\n validate: objectOf({\n id: string,\n emoticon: string\n }),\n process ({ data, meta, contractID }, { state }) {\n if (!state.saveMessage) {\n return\n }\n const { id, emoticon } = data\n const msgIndex = findMessageIdx(id, state.messages)\n if (msgIndex >= 0) {\n let emoticons = cloneDeep(state.messages[msgIndex].emoticons || {})\n if (emoticons[emoticon]) {\n const alreadyAdded = emoticons[emoticon].indexOf(meta.username)\n if (alreadyAdded >= 0) {\n emoticons[emoticon].splice(alreadyAdded, 1)\n if (!emoticons[emoticon].length) {\n delete emoticons[emoticon]\n if (!Object.keys(emoticons).length) {\n emoticons = null\n }\n }\n } else {\n emoticons[emoticon].push(meta.username)\n }\n } else {\n emoticons[emoticon] = [meta.username]\n }\n if (emoticons) {\n Vue.set(state.messages[msgIndex], 'emoticons', emoticons)\n } else {\n Vue.delete(state.messages[msgIndex], 'emoticons')\n }\n }\n },\n sideEffect ({ contractID, hash }) {\n emitMessageEvent({ contractID, hash })\n }\n }\n }\n})\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/group.js b/test/contracts/group.js index 06f1632798..dd7efefac0 100644 --- a/test/contracts/group.js +++ b/test/contracts/group.js @@ -1113,7 +1113,10 @@ function memberLeaves({ username, dateLeft }, { meta, state, getters }) { updateCurrentDistribution({ meta, state, getters }); } function isActionYoungerThanUser(actionMeta, userProfile) { - return Boolean(userProfile) && compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0; + if (!userProfile) { + return false; + } + return compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0; } module_default("chelonia/defineContract", { name: "gi.contracts/group", diff --git a/test/contracts/group.js.map b/test/contracts/group.js.map index b19764dde2..e95318080f 100644 --- a/test/contracts/group.js.map +++ b/test/contracts/group.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/common/errors.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/shared/voting/rules.js", "../../frontend/model/contracts/shared/time.js", "../../frontend/model/contracts/shared/voting/proposals.js", "../../frontend/model/contracts/shared/payments/index.js", "../../frontend/model/contracts/shared/distribution/mincome-proportional.js", "../../frontend/model/contracts/shared/distribution/payments-minimizer.js", "../../frontend/model/contracts/shared/currencies.js", "../../frontend/model/contracts/shared/distribution/distribution.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/group.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nexport class GIErrorIgnoreAndBan extends Error {\n // ugly boilerplate because JavaScript is stupid\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types\n constructor (...params ) {\n super(...params)\n this.name = 'GIErrorIgnoreAndBan'\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n\n// Used to throw human readable errors on UI.\nexport class GIErrorUIRuntimeError extends Error {\n constructor (...params ) {\n super(...params)\n // this.name = this.constructor.name\n this.name = 'GIErrorUIRuntimeError' // string literal so minifier doesn't overwrite\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n", "'use strict'\n\n \n \n \n \n\nexport default function mincomeProportional (haveNeeds ) {\n let totalHave = 0\n let totalNeed = 0\n const havers = []\n const needers = []\n for (const haveNeed of haveNeeds) {\n if (haveNeed.haveNeed > 0) {\n havers.push(haveNeed)\n totalHave += haveNeed.haveNeed\n } else if (haveNeed.haveNeed < 0) {\n needers.push(haveNeed)\n totalNeed += Math.abs(haveNeed.haveNeed)\n }\n }\n const totalPercent = Math.min(1, totalNeed / totalHave)\n const payments = []\n for (const haver of havers) {\n const distributionAmount = totalPercent * haver.haveNeed\n for (const needer of needers) {\n const belowPercentage = Math.abs(needer.haveNeed) / totalNeed\n payments.push({\n amount: distributionAmount * belowPercentage,\n from: haver.name,\n to: needer.name\n })\n }\n }\n return payments\n}\n", "'use strict'\n\n// greedy algorithm responsible for \"balancing\" payments\n// such that the least number of payments are made.\nexport default function minimizeTotalPaymentsCount (\n distribution \n) {\n const neederTotalReceived = {}\n const haverTotalHave = {}\n const haversSorted = []\n const needersSorted = []\n const minimizedDistribution = []\n for (const todo of distribution) {\n neederTotalReceived[todo.to] = (neederTotalReceived[todo.to] || 0) + todo.amount\n haverTotalHave[todo.from] = (haverTotalHave[todo.from] || 0) + todo.amount\n }\n for (const name in haverTotalHave) {\n haversSorted.push({ name, amount: haverTotalHave[name] })\n }\n for (const name in neederTotalReceived) {\n needersSorted.push({ name, amount: neederTotalReceived[name] })\n }\n // sort haves and needs: greatest to least\n haversSorted.sort((a, b) => b.amount - a.amount)\n needersSorted.sort((a, b) => b.amount - a.amount)\n while (haversSorted.length > 0 && needersSorted.length > 0) {\n const mostHaver = haversSorted.pop()\n const mostNeeder = needersSorted.pop()\n const diff = mostHaver.amount - mostNeeder.amount\n if (diff < 0) {\n // we used up everything the haver had\n minimizedDistribution.push({ amount: mostHaver.amount, from: mostHaver.name, to: mostNeeder.name })\n mostNeeder.amount -= mostHaver.amount\n needersSorted.push(mostNeeder)\n } else if (diff > 0) {\n // we completely filled up the needer's need and still have some left over\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n mostHaver.amount -= mostNeeder.amount\n haversSorted.push(mostHaver)\n } else {\n // a perfect match\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n }\n }\n return minimizedDistribution\n}\n", "'use strict'\n\n \n \n \n \n \n \n \n \n\n// https://github.com/okTurtles/group-income/issues/813#issuecomment-593680834\n// round all accounting to DECIMALS_MAX decimal places max to avoid consensus\n// issues that can arise due to different floating point values\n// at extreme precisions. If this becomes inadequate, instead of increasing\n// this value, switch to a different currency base, e.g. from BTC to mBTC.\nexport const DECIMALS_MAX = 8\n\nfunction commaToDots (value ) {\n // ex: \"1,55\" -> \"1.55\"\n return typeof value === 'string' ? value.replace(/,/, '.') : value.toString()\n}\n\nfunction isNumeric (nr ) {\n return !isNaN((nr ) - parseFloat(nr))\n}\n\nfunction isInDecimalsLimit (nr , decimalsMax ) {\n const decimals = nr.split('.')[1]\n return !decimals || decimals.length <= decimalsMax\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nfunction validateMincome (value , decimalsMax ) {\n const nr = commaToDots(value)\n return isNumeric(nr) && isInDecimalsLimit(nr, decimalsMax)\n}\n\nfunction decimalsOrInt (num , decimalsMax ) {\n // ex: 12.5 -> \"12.50\", but 250 -> \"250\"\n return num.toFixed(decimalsMax).replace(/\\.0+$/, '')\n}\n\nexport function saferFloat (value ) {\n // ex: 1.333333333333333333 -> 1.33333333\n return parseFloat(value.toFixed(DECIMALS_MAX))\n}\n\nexport function normalizeCurrency (value ) {\n // ex: \"1,333333333333333333\" -> 1.33333333\n return saferFloat(parseFloat(commaToDots(value)))\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nexport function mincomePositive (value ) {\n return parseFloat(commaToDots(value)) > 0\n}\n\nfunction makeCurrency (options) {\n const { symbol, symbolWithCode, decimalsMax, formatCurrency } = options\n return {\n symbol,\n symbolWithCode,\n decimalsMax,\n displayWithCurrency: (n ) => formatCurrency(decimalsOrInt(n, decimalsMax)),\n displayWithoutCurrency: (n ) => decimalsOrInt(n, decimalsMax),\n validate: (n ) => validateMincome(n, decimalsMax)\n }\n}\n\n// NOTE: if we needed for some reason, this could also be defined in\n// a json file that's read in and generates this object. For\n// example, that would allow the addition of currencies without\n// having to \"recompile\" a new version of the app.\nconst currencies = {\n USD: makeCurrency({\n symbol: '$',\n symbolWithCode: '$ USD',\n decimalsMax: 2,\n formatCurrency: amount => '$' + amount\n }),\n EUR: makeCurrency({\n symbol: '\u20AC',\n symbolWithCode: '\u20AC EUR',\n decimalsMax: 2,\n formatCurrency: amount => '\u20AC' + amount\n }),\n BTC: makeCurrency({\n symbol: '\u0243',\n symbolWithCode: '\u0243 BTC',\n decimalsMax: DECIMALS_MAX,\n formatCurrency: amount => amount + '\u0243'\n })\n}\n\nexport default currencies\n", "'use strict'\n\nimport mincomeProportional from './mincome-proportional.js'\nimport minimizeTotalPaymentsCount from './payments-minimizer.js'\nimport { cloneDeep } from '../giLodash.js'\nimport { saferFloat, DECIMALS_MAX } from '../currencies.js'\n\n \n\nconst tinyNum = 1 / Math.pow(10, DECIMALS_MAX)\n\nexport function unadjustedDistribution ({ haveNeeds = [], minimize = true } \n \n ) {\n const distribution = mincomeProportional(haveNeeds)\n return minimize ? minimizeTotalPaymentsCount(distribution) : distribution\n}\n\nexport function adjustedDistribution (\n { distribution, payments, dueOn } \n) {\n distribution = cloneDeep(distribution)\n // ensure the total is set because of how reduceDistribution works\n for (const todo of distribution) {\n todo.total = todo.amount\n }\n distribution = subtractDistributions(distribution, payments)\n // remove any todos for containing miniscule amounts\n // and pledgers who switched sides should have their todos removed\n .filter(todo => todo.amount >= tinyNum)\n for (const todo of distribution) {\n todo.amount = saferFloat(todo.amount)\n todo.total = saferFloat(todo.total)\n todo.partial = todo.total !== todo.amount\n todo.isLate = false\n todo.dueOn = dueOn\n }\n // TODO: add in latePayments to the end of the distribution\n // consider passing in latePayments\n return distribution\n}\n\n// Merges multiple payments between any combinations two of users:\nfunction reduceDistribution (payments ) {\n // Don't modify the payments list/object parameter in-place, as this is not intended:\n payments = cloneDeep(payments)\n for (let i = 0; i < payments.length; i++) {\n const paymentA = payments[i]\n for (let j = i + 1; j < payments.length; j++) {\n const paymentB = payments[j]\n\n // Were paymentA and paymentB between the same two users?\n if ((paymentA.from === paymentB.from && paymentA.to === paymentB.to) ||\n (paymentA.to === paymentB.from && paymentA.from === paymentB.to)) {\n // Add or subtract paymentB's amount to paymentA's amount, depending on the relative\n // direction of the two payments:\n paymentA.amount += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.amount\n paymentA.total += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.total\n // Remove paymentB from payments, and decrement the inner sentinal loop variable:\n payments.splice(j, 1)\n j--\n }\n }\n }\n return payments\n}\n\nfunction addDistributions (paymentsA , paymentsB ) {\n return reduceDistribution([...paymentsA, ...paymentsB])\n}\n\nfunction subtractDistributions (paymentsA , paymentsB ) {\n // Don't modify any payment list/objects parameters in-place, as this is not intended:\n paymentsB = cloneDeep(paymentsB)\n // Reverse the sign of the second operand's amounts so that the final addition is actually subtraction:\n for (const p of paymentsB) {\n p.amount *= -1\n p.total *= -1\n }\n return addDistributions(paymentsA, paymentsB)\n}\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, Errors, L } from '@common/common.js'\nimport votingRules, { ruleType, VOTE_FOR, VOTE_AGAINST, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './shared/voting/rules.js'\nimport proposals, { proposalType, proposalSettingsType, archiveProposal } from './shared/voting/proposals.js'\nimport {\n PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC, STATUS_OPEN, STATUS_CANCELLED, MAX_ARCHIVED_PROPOSALS, PROPOSAL_ARCHIVED,\n INVITE_INITIAL_CREATOR, INVITE_STATUS, PROFILE_STATUS, INVITE_EXPIRES_IN_DAYS\n} from './shared/constants.js'\nimport { paymentStatusType, paymentType, PAYMENT_COMPLETED } from './shared/payments/index.js'\nimport { merge, deepEqualJSONType, omit } from './shared/giLodash.js'\nimport { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, periodStampGivenDate, dateIsWithinPeriod, DAYS_MILLIS } from './shared/time.js'\nimport { unadjustedDistribution, adjustedDistribution } from './shared/distribution/distribution.js'\nimport currencies, { saferFloat } from './shared/currencies.js'\nimport { inviteType, chatRoomAttributesType } from './shared/types.js'\nimport { arrayOf, mapOf, objectOf, objectMaybeOf, optional, string, number, boolean, object, unionOf, tupleOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction vueFetchInitKV (obj , key , initialValue ) {\n let value = obj[key]\n if (!value) {\n Vue.set(obj, key, initialValue)\n value = obj[key]\n }\n return value\n}\n\nfunction initGroupProfile (contractID , joinedDate ) {\n return {\n globalUsername: '', // TODO: this? e.g. groupincome:greg / namecoin:bob / ens:alice\n contractID,\n joinedDate,\n nonMonetaryContributions: [],\n status: PROFILE_STATUS.ACTIVE,\n departedDate: null\n }\n}\n\nfunction initPaymentPeriod ({ getters }) {\n return {\n // this saved so that it can be used when creating a new payment\n initialCurrency: getters.groupMincomeCurrency,\n // TODO: should we also save the first period's currency exchange rate..?\n // all payments during the period use this to set their exchangeRate\n // see notes and code in groupIncomeAdjustedDistribution for details.\n // TODO: for the currency change proposal, have it update the mincomeExchangeRate\n // using .mincomeExchangeRate *= proposal.exchangeRate\n mincomeExchangeRate: 1, // modified by proposals to change mincome currency\n paymentsFrom: {}, // fromUser => toUser => Array\n // snapshot of adjusted distribution after each completed payment\n // yes, it is possible a payment began in one period and completed in another\n // in which case lastAdjustedDistribution for the previous period will be updated\n lastAdjustedDistribution: null,\n // snapshot of haveNeeds. made only when there are no payments\n haveNeedsSnapshot: null\n }\n}\n\n// NOTE: do not call any of these helper functions from within a getter b/c they modify state!\n\nfunction clearOldPayments ({ state, getters }) {\n const sortedPeriodKeys = Object.keys(state.paymentsByPeriod).sort()\n // save two periods worth of payments, max\n while (sortedPeriodKeys.length > 2) {\n const period = sortedPeriodKeys.shift()\n for (const paymentHash of getters.paymentHashesForPeriod(period)) {\n Vue.delete(state.payments, paymentHash)\n // TODO: archive the old payments in a sideEffect, not here\n }\n Vue.delete(state.paymentsByPeriod, period)\n }\n}\n\nfunction initFetchPeriodPayments ({ meta, state, getters }) {\n const period = getters.periodStampGivenDate(meta.createdDate)\n const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ getters }))\n clearOldPayments({ state, getters })\n return periodPayments\n}\n\n// this function is called each time a payment is completed or a user adjusts their income details.\n// TODO: call also when mincome is adjusted\nfunction updateCurrentDistribution ({ meta, state, getters }) {\n const curPeriodPayments = initFetchPeriodPayments({ meta, state, getters })\n const period = getters.periodStampGivenDate(meta.createdDate)\n const noPayments = Object.keys(curPeriodPayments.paymentsFrom).length === 0\n // update distributionDate if we've passed into the next period\n if (comparePeriodStamps(period, getters.groupSettings.distributionDate) > 0) {\n getters.groupSettings.distributionDate = period\n }\n // save haveNeeds if there are no payments or the haveNeeds haven't been saved yet\n if (noPayments || !curPeriodPayments.haveNeedsSnapshot) {\n curPeriodPayments.haveNeedsSnapshot = getters.haveNeedsForThisPeriod(period)\n }\n // if there are payments this period, save the adjusted distribution\n if (!noPayments) {\n updateAdjustedDistribution({ period, getters })\n }\n}\n\nfunction updateAdjustedDistribution ({ period, getters }) {\n const payments = getters.groupPeriodPayments[period]\n if (payments && payments.haveNeedsSnapshot) {\n const minimize = getters.groupSettings.minimizeDistribution\n payments.lastAdjustedDistribution = adjustedDistribution({\n distribution: unadjustedDistribution({ haveNeeds: payments.haveNeedsSnapshot, minimize }),\n payments: getters.paymentsForPeriod(period),\n dueOn: getters.dueDateForPeriod(period)\n }).filter(todo => {\n // only return todos for active members\n return getters.groupProfile(todo.to).status === PROFILE_STATUS.ACTIVE\n })\n }\n}\n\nfunction memberLeaves ({ username, dateLeft }, { meta, state, getters }) {\n state.profiles[username].status = PROFILE_STATUS.REMOVED\n state.profiles[username].departedDate = dateLeft\n // remove any todos for this member from the adjusted distribution\n updateCurrentDistribution({ meta, state, getters })\n}\n\nfunction isActionYoungerThanUser (actionMeta , userProfile ) {\n // A util function that checks if an action (or event) in a group occurred after a particular user joined a group.\n // This is used mostly for checking if a notification should be sent for that user or not.\n // e.g.) user-2 who joined a group later than user-1 (who is the creator of the group) doesn't need to receive\n // 'MEMBER_ADDED' notification for user-1.\n // In some situations, userProfile is undefined, for example, when inviteAccept is called in\n // certain situations. So we need to check for that here.\n return Boolean(userProfile) && compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/group',\n metadata: {\n validate: objectOf({\n createdDate: string,\n username: string,\n identityContractID: string\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n // TODO: We may want to get the time from the server instead of relying on\n // the client in case the client's clock isn't set correctly.\n // the only issue here is that it involves an async function...\n // See: https://github.com/okTurtles/group-income/issues/531\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n // These getters are restricted only to the contract's state.\n // Do not access state outside the contract state inside of them.\n // For example, if the getter you use tries to access `state.loggedIn`,\n // that will break the `latestContractState` function in state.js.\n // It is only safe to access state outside of the contract in a contract action's\n // `sideEffect` function (as long as it doesn't modify contract state)\n getters: {\n // we define `currentGroupState` here so that we can redefine it in state.js\n // so that we can re-use these getter definitions in state.js since they are\n // compatible with Vuex getter definitions.\n // Here `state` refers to the individual group contract's state, the equivalent\n // of `vuexRootState[someGroupContractID]`.\n currentGroupState (state) {\n return state\n },\n groupSettings (state, getters) {\n return getters.currentGroupState.settings || {}\n },\n groupProfile (state, getters) {\n return username => {\n const profiles = getters.currentGroupState.profiles\n return profiles && profiles[username]\n }\n },\n groupProfiles (state, getters) {\n const profiles = {}\n for (const username in (getters.currentGroupState.profiles || {})) {\n const profile = getters.groupProfile(username)\n if (profile.status === PROFILE_STATUS.ACTIVE) {\n profiles[username] = profile\n }\n }\n return profiles\n },\n groupMincomeAmount (state, getters) {\n return getters.groupSettings.mincomeAmount\n },\n groupMincomeCurrency (state, getters) {\n return getters.groupSettings.mincomeCurrency\n },\n periodStampGivenDate (state, getters) {\n return (recentDate ) => {\n if (typeof recentDate !== 'string') {\n recentDate = recentDate.toISOString()\n }\n const { distributionDate, distributionPeriodLength } = getters.groupSettings\n return periodStampGivenDate({\n recentDate,\n periodStart: distributionDate,\n periodLength: distributionPeriodLength\n })\n }\n },\n periodBeforePeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -len))\n }\n },\n periodAfterPeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), len))\n }\n },\n dueDateForPeriod (state, getters) {\n return (periodStamp ) => {\n return dateToPeriodStamp(\n addTimeToDate(\n dateFromPeriodStamp(getters.periodAfterPeriod(periodStamp)),\n -DAYS_MILLIS\n )\n )\n }\n },\n paymentTotalFromUserToUser (state, getters) {\n return (fromUser, toUser, periodStamp) => {\n const payments = getters.currentGroupState.payments\n const periodPayments = getters.groupPeriodPayments\n const { paymentsFrom, mincomeExchangeRate } = periodPayments[periodStamp] || {}\n // NOTE: @babel/plugin-proposal-optional-chaining would come in super-handy\n // here, but I couldn't get it to work with our linter. :(\n // https://github.com/babel/babel-eslint/issues/511\n const total = (((paymentsFrom || {})[fromUser] || {})[toUser] || []).reduce((a, hash) => {\n const payment = payments[hash]\n let { amount, exchangeRate, status } = payment.data\n if (status !== PAYMENT_COMPLETED) {\n return a\n }\n const paymentCreatedPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n // if this payment is from a previous period, then make sure to take into account\n // any proposals that passed in between the payment creation and the payment\n // completion that modified the group currency by multiplying both period's\n // exchange rates\n if (periodStamp !== paymentCreatedPeriodStamp) {\n if (paymentCreatedPeriodStamp !== getters.periodBeforePeriod(periodStamp)) {\n console.warn(`paymentTotalFromUserToUser: super old payment shouldn't exist, ignoring! (curPeriod=${periodStamp})`, JSON.stringify(payment))\n return a\n }\n exchangeRate *= periodPayments[paymentCreatedPeriodStamp].mincomeExchangeRate\n }\n return a + (amount * exchangeRate * mincomeExchangeRate)\n }, 0)\n return saferFloat(total)\n }\n },\n paymentHashesForPeriod (state, getters) {\n return (periodStamp) => {\n const periodPayments = getters.groupPeriodPayments[periodStamp]\n if (periodPayments) {\n let hashes = []\n const { paymentsFrom } = periodPayments\n for (const fromUser in paymentsFrom) {\n for (const toUser in paymentsFrom[fromUser]) {\n hashes = hashes.concat(paymentsFrom[fromUser][toUser])\n }\n }\n return hashes\n }\n }\n },\n groupMembersByUsername (state, getters) {\n return Object.keys(getters.groupProfiles)\n },\n groupMembersCount (state, getters) {\n return getters.groupMembersByUsername.length\n },\n groupMembersPending (state, getters) {\n const invites = getters.currentGroupState.invites\n const pendingMembers = {}\n for (const inviteId in invites) {\n const invite = invites[inviteId]\n if (\n invite.status === INVITE_STATUS.VALID &&\n invite.creator !== INVITE_INITIAL_CREATOR\n ) {\n pendingMembers[invites[inviteId].invitee] = {\n invitedBy: invites[inviteId].creator,\n expires: invite.expires\n }\n }\n }\n return pendingMembers\n },\n groupShouldPropose (state, getters) {\n return getters.groupMembersCount >= 3\n },\n groupProposalSettings (state, getters) {\n return (proposalType = PROPOSAL_GENERIC) => {\n return getters.groupSettings.proposals[proposalType]\n }\n },\n groupCurrency (state, getters) {\n const mincomeCurrency = getters.groupMincomeCurrency\n return mincomeCurrency && currencies[mincomeCurrency]\n },\n groupMincomeFormatted (state, getters) {\n return getters.withGroupCurrency?.(getters.groupMincomeAmount)\n },\n groupMincomeSymbolWithCode (state, getters) {\n return getters.groupCurrency?.symbolWithCode\n },\n groupPeriodPayments (state, getters) {\n // note: a lot of code expects this to return an object, so keep the || {} below\n return getters.currentGroupState.paymentsByPeriod || {}\n },\n groupThankYousFrom (state, getters) {\n return getters.currentGroupState.thankYousFrom || {}\n },\n withGroupCurrency (state, getters) {\n // TODO: If this group has no defined mincome currency, not even a default one like\n // USD, then calling this function is probably an error which should be reported.\n // Just make sure the UI doesn't break if an exception is thrown, since this is\n // bound to the UI in some location.\n return getters.groupCurrency?.displayWithCurrency\n },\n getChatRooms (state, getters) {\n return getters.currentGroupState.chatRooms\n },\n generalChatRoomId (state, getters) {\n return getters.currentGroupState.generalChatRoomId\n },\n // getter is named haveNeedsForThisPeriod instead of haveNeedsForPeriod because it uses\n // getters.groupProfiles - and that is always based on the most recent values. we still\n // pass in the current period because it's used to set the \"when\" property\n haveNeedsForThisPeriod (state, getters) {\n return (currentPeriod ) => {\n // NOTE: if we ever switch back to the \"real-time\" adjusted distribution algorithm,\n // make sure that this function also handles userExitsGroupEvent\n const groupProfiles = getters.groupProfiles // TODO: these should use the haveNeeds for the specific period's distribution period\n const haveNeeds = []\n for (const username in groupProfiles) {\n const { incomeDetailsType, joinedDate } = groupProfiles[username]\n if (incomeDetailsType) {\n const amount = groupProfiles[username][incomeDetailsType]\n const haveNeed = incomeDetailsType === 'incomeAmount' ? amount - getters.groupMincomeAmount : amount\n // construct 'when' this way in case we ever use a pro-rated algorithm\n let when = dateFromPeriodStamp(currentPeriod).toISOString()\n if (dateIsWithinPeriod({\n date: joinedDate,\n periodStart: currentPeriod,\n periodLength: getters.groupSettings.distributionPeriodLength\n })) {\n when = joinedDate\n }\n haveNeeds.push({ name: username, haveNeed, when })\n }\n }\n return haveNeeds\n }\n },\n paymentsForPeriod (state, getters) {\n return (periodStamp) => {\n const hashes = getters.paymentHashesForPeriod(periodStamp)\n const events = []\n if (hashes && hashes.length > 0) {\n const payments = getters.currentGroupState.payments\n for (const paymentHash of hashes) {\n const payment = payments[paymentHash]\n if (payment.data.status === PAYMENT_COMPLETED) {\n events.push({\n from: payment.meta.username,\n to: payment.data.toUser,\n hash: paymentHash,\n amount: payment.data.amount,\n isLate: !!payment.data.isLate,\n when: payment.data.completedDate\n })\n }\n }\n }\n return events\n }\n }\n // distributionEventsForMonth (state, getters) {\n // return (monthstamp) => {\n // // NOTE: if we ever switch back to the \"real-time\" adjusted distribution\n // // algorithm, make sure that this function also handles userExitsGroupEvent\n // const distributionEvents = getters.haveNeedEventsForMonth(monthstamp)\n // const paymentEvents = getters.paymentEventsForMonth(monthstamp)\n // distributionEvents.splice(distributionEvents.length, 0, paymentEvents)\n // return distributionEvents.sort((a, b) => compareISOTimestamps(a.data.when, b.data.when))\n // }\n // }\n },\n // NOTE: All mutations must be atomic in their edits of the contract state.\n // THEY ARE NOT to farm out any further mutations through the async actions!\n actions: {\n // this is the constructor\n 'gi.contracts/group': {\n validate: objectMaybeOf({\n invites: mapOf(string, inviteType),\n settings: objectMaybeOf({\n // TODO: add 'groupPubkey'\n groupName: string,\n groupPicture: string,\n sharedValues: string,\n mincomeAmount: number,\n mincomeCurrency: string,\n distributionDate: isPeriodStamp,\n distributionPeriodLength: number,\n minimizeDistribution: boolean,\n proposals: objectOf({\n [PROPOSAL_INVITE_MEMBER]: proposalSettingsType,\n [PROPOSAL_REMOVE_MEMBER]: proposalSettingsType,\n [PROPOSAL_GROUP_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_GENERIC]: proposalSettingsType\n })\n })\n }),\n process ({ data, meta }, { state, getters }) {\n // TODO: checkpointing: https://github.com/okTurtles/group-income/issues/354\n const initialState = merge({\n payments: {},\n paymentsByPeriod: {},\n thankYousFrom: {}, // { fromUser1: { toUser1: msg1, toUser2: msg2, ... }, fromUser2: {}, ... }\n invites: {},\n proposals: {}, // hashes => {} TODO: this, see related TODOs in GroupProposal\n settings: {\n groupCreator: meta.username,\n distributionPeriodLength: 30 * DAYS_MILLIS,\n inviteExpiryOnboarding: INVITE_EXPIRES_IN_DAYS.ON_BOARDING,\n inviteExpiryProposal: INVITE_EXPIRES_IN_DAYS.PROPOSAL\n },\n profiles: {\n [meta.username]: initGroupProfile(meta.identityContractID, meta.createdDate)\n },\n chatRooms: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n initFetchPeriodPayments({ meta, state, getters })\n }\n },\n 'gi.contracts/group/payment': {\n validate: objectMaybeOf({\n // TODO: how to handle donations to okTurtles?\n // TODO: how to handle payments to groups or users outside of this group?\n toUser: string,\n amount: number,\n currencyFromTo: tupleOf(string, string), // must be one of the keys in currencies.js (e.g. USD, EUR, etc.) TODO: handle old clients not having one of these keys, see OP_PROTOCOL_UPGRADE https://github.com/okTurtles/group-income/issues/603\n // multiply 'amount' by 'exchangeRate', which must always be\n // based on the initialCurrency of the period in which this payment was created.\n // it is then further multiplied by the period's 'mincomeExchangeRate', which\n // is modified if any proposals pass to change the mincomeCurrency\n exchangeRate: number,\n txid: string,\n status: paymentStatusType,\n paymentType: paymentType,\n details: optional(object),\n memo: optional(string)\n }),\n process ({ data, meta, hash }, { state, getters }) {\n if (data.status === PAYMENT_COMPLETED) {\n console.error(`payment: payment ${hash} cannot have status = 'completed'!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('payments cannot be instantly completed!')\n }\n Vue.set(state.payments, hash, {\n data: {\n ...data,\n groupMincome: getters.groupMincomeAmount\n },\n meta,\n history: [[meta.createdDate, hash]]\n })\n const { paymentsFrom } = initFetchPeriodPayments({ meta, state, getters })\n const fromUser = vueFetchInitKV(paymentsFrom, meta.username, {})\n const toUser = vueFetchInitKV(fromUser, data.toUser, [])\n toUser.push(hash)\n // TODO: handle completed payments here too! (for manual payment support)\n }\n },\n 'gi.contracts/group/paymentUpdate': {\n validate: objectMaybeOf({\n paymentHash: string,\n updatedProperties: objectMaybeOf({\n status: paymentStatusType,\n details: object,\n memo: string\n })\n }),\n process ({ data, meta, hash }, { state, getters }) {\n // TODO: we don't want to keep a history of all payments in memory all the time\n // https://github.com/okTurtles/group-income/issues/426\n const payment = state.payments[data.paymentHash]\n // TODO: move these types of validation errors into the validate function so\n // that they can be done before sending as well as upon receiving\n if (!payment) {\n console.error(`paymentUpdate: no payment ${data.paymentHash}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate without existing payment')\n }\n // if the payment is being modified by someone other than the person who sent or received it, throw an exception\n if (meta.username !== payment.meta.username && meta.username !== payment.data.toUser) {\n console.error(`paymentUpdate: bad username ${meta.username} != ${payment.meta.username} != ${payment.data.username}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate from bad user!')\n }\n payment.history.push([meta.createdDate, hash])\n merge(payment.data, data.updatedProperties)\n // we update \"this period\"'s snapshot 'lastAdjustedDistribution' on each completed payment\n if (data.updatedProperties.status === PAYMENT_COMPLETED) {\n payment.data.completedDate = meta.createdDate\n // update the current distribution unless this update is for a payment from the previous period\n const updatePeriodStamp = getters.periodStampGivenDate(meta.createdDate)\n const paymentPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n if (comparePeriodStamps(updatePeriodStamp, paymentPeriodStamp) > 0) {\n updateAdjustedDistribution({ period: paymentPeriodStamp, getters })\n } else {\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n },\n sideEffect ({ meta, contractID, data }, { state, getters }) {\n if (data.updatedProperties.status === PAYMENT_COMPLETED) {\n const { loggedIn } = sbp('state/vuex/state')\n const payment = state.payments[data.paymentHash]\n\n if (loggedIn.username === payment.data.toUser) {\n sbp('gi.notifications/emit', 'PAYMENT_RECEIVED', {\n groupID: contractID,\n creator: meta.username,\n paymentHash: data.paymentHash,\n amount: getters.withGroupCurrency(payment.data.amount)\n })\n }\n }\n }\n },\n 'gi.contracts/group/sendPaymentThankYou': {\n validate: objectOf({\n fromUser: string,\n toUser: string,\n memo: string\n }),\n process ({ data }, { state }) {\n const fromUser = vueFetchInitKV(state.thankYousFrom, data.fromUser, {})\n Vue.set(fromUser, data.toUser, data.memo)\n },\n sideEffect ({ contractID, meta, data }) {\n const { loggedIn } = sbp('state/vuex/state')\n\n if (data.toUser === loggedIn.username) {\n sbp('gi.notifications/emit', 'PAYMENT_THANKYOU_SENT', {\n groupID: contractID,\n creator: meta.username, // username of the from user. to be used with sbp('namespace/lookup') in 'AvatarUser.vue'\n fromUser: data.fromUser, // display name of the from user\n toUser: data.toUser\n })\n }\n }\n },\n 'gi.contracts/group/proposal': {\n validate: (data, { state, meta }) => {\n objectOf({\n proposalType: proposalType,\n proposalData: object, // data for Vue widgets\n votingRule: ruleType,\n expires_date_ms: number // calculate by grabbing proposal expiry from group properties and add to `meta.createdDate`\n })(data)\n\n const dataToCompare = omit(data.proposalData, ['reason'])\n\n // Validate this isn't a duplicate proposal\n for (const hash in state.proposals) {\n const prop = state.proposals[hash]\n if (prop.status !== STATUS_OPEN || prop.data.proposalType !== data.proposalType) {\n continue\n }\n\n if (deepEqualJSONType(omit(prop.data.proposalData, ['reason']), dataToCompare)) {\n throw new TypeError(L('There is an identical open proposal.'))\n }\n\n // TODO - verify if type of proposal already exists (SETTING_CHANGE).\n }\n },\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.proposals, hash, {\n data,\n meta,\n votes: { [meta.username]: VOTE_FOR },\n status: STATUS_OPEN,\n payload: null // set later by group/proposalVote\n })\n // TODO: save all proposals disk so that we only keep open proposals in memory\n // TODO: create a global timer to auto-pass/archive expired votes\n // make sure to set that proposal's status as STATUS_EXPIRED if it's expired\n },\n sideEffect ({ contractID, meta, data }, { getters }) {\n const { loggedIn } = sbp('state/vuex/state')\n const typeToSubTypeMap = {\n [PROPOSAL_INVITE_MEMBER]: 'ADD_MEMBER',\n [PROPOSAL_REMOVE_MEMBER]: 'REMOVE_MEMBER',\n [PROPOSAL_GROUP_SETTING_CHANGE]: 'CHANGE_MINCOME',\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: 'CHANGE_VOTING_RULE',\n [PROPOSAL_GENERIC]: 'GENERIC'\n }\n\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'NEW_PROPOSAL', {\n groupID: contractID,\n creator: meta.username,\n subtype: typeToSubTypeMap[data.proposalType]\n })\n }\n }\n },\n 'gi.contracts/group/proposalVote': {\n validate: objectOf({\n proposalHash: string,\n vote: string,\n passPayload: optional(unionOf(object, string)) // TODO: this, somehow we need to send an OP_KEY_ADD GIMessage to add a generated once-only writeonly message public key to the contract, and (encrypted) include the corresponding invite link, also, we need all clients to verify that this message/operation was valid to prevent a hacked client from adding arbitrary OP_KEY_ADD messages, and automatically ban anyone generating such messages\n }),\n process (message, { state }) {\n const { data, hash, meta } = message\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalVote: no proposal for ${data.proposalHash}!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n }\n Vue.set(proposal.votes, meta.username, data.vote)\n // TODO: handle vote pass/fail\n // check if proposal is expired, if so, ignore (but log vote)\n if (new Date(meta.createdDate).getTime() > proposal.data.expires_date_ms) {\n console.warn('proposalVote: vote on expired proposal!', { proposal, data, meta })\n // TODO: display warning or something\n return\n }\n // see if this is a deciding vote\n const result = votingRules[proposal.data.votingRule](state, proposal.data.proposalType, proposal.votes)\n if (result === VOTE_FOR || result === VOTE_AGAINST) {\n // handles proposal pass or fail, will update proposal.status accordingly\n proposals[proposal.data.proposalType][result](state, message)\n Vue.set(proposal, 'dateClosed', meta.createdDate)\n }\n },\n sideEffect ({ contractID, data, meta }, { state, getters }) {\n const proposal = state.proposals[data.proposalHash]\n const { loggedIn } = sbp('state/vuex/state')\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (proposal?.dateClosed &&\n isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'PROPOSAL_CLOSED', {\n groupID: contractID,\n creator: meta.username,\n proposalStatus: proposal.status\n })\n }\n }\n },\n 'gi.contracts/group/proposalCancel': {\n validate: objectOf({\n proposalHash: string\n }),\n process ({ data, meta, contractID }, { state }) {\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalCancel: no proposal for ${data.proposalHash}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n } else if (proposal.meta.username !== meta.username) {\n console.error(`proposalCancel: proposal ${data.proposalHash} belongs to ${proposal.meta.username} not ${meta.username}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalWithdraw for wrong user!')\n }\n Vue.set(proposal, 'status', STATUS_CANCELLED)\n archiveProposal({ state, proposalHash: data.proposalHash, proposal, contractID })\n }\n },\n 'gi.contracts/group/removeMember': {\n validate: (data, { state, getters, meta }) => {\n objectOf({\n member: string, // username to remove\n reason: optional(string),\n automated: optional(boolean),\n // In case it happens in a big group (by proposal)\n // we need to validate the associated proposal.\n proposalHash: optional(string),\n proposalPayload: optional(objectOf({\n secret: string // NOTE: simulate the OP_KEY_* stuff for now\n }))\n })(data)\n\n const memberToRemove = data.member\n const membersCount = getters.groupMembersCount\n\n if (!state.profiles[memberToRemove]) {\n throw new TypeError(L('Not part of the group.'))\n }\n if (membersCount === 1 || memberToRemove === meta.username) {\n throw new TypeError(L('Cannot remove yourself.'))\n }\n\n if (membersCount < 3) {\n // In a small group only the creator can remove someone\n // TODO: check whether meta.username has required admin permissions\n if (meta.username !== state.settings.groupCreator) {\n throw new TypeError(L('Only the group creator can remove members.'))\n }\n } else {\n // In a big group a removal can only happen through a proposal\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // TODO this\n throw new TypeError(L('Admin credentials needed and not implemented yet.'))\n }\n\n if (!proposal.payload || proposal.payload.secret !== data.proposalPayload.secret) {\n throw new TypeError(L('Invalid associated proposal.'))\n }\n }\n },\n process ({ data, meta }, { state, getters }) {\n memberLeaves(\n { username: data.member, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n },\n sideEffect ({ data, meta, contractID }, { state, getters }) {\n const rootState = sbp('state/vuex/state')\n const contracts = rootState.contracts || {}\n const { username } = rootState.loggedIn\n\n if (data.member === username) {\n // If this member is re-joining the group, ignore the rest\n // so the member doesn't remove themself again.\n if (sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n return\n }\n\n const groupIdToSwitch = Object.keys(contracts)\n .find(cID => contracts[cID].type === 'gi.contracts/group' &&\n cID !== contractID && rootState[cID].settings) || null\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {})\n sbp('state/vuex/commit', 'setCurrentGroupId', groupIdToSwitch)\n // we can't await on this in here, because it will cause a deadlock, since Chelonia processes\n // this sideEffect on the eventqueue for this contractID, and /remove uses that same eventqueue\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown by /remove ${contractID}:`, e)\n })\n // this looks crazy, but doing this was necessary to fix a race condition in the\n // group-member-removal Cypress tests where due to the ordering of asynchronous events\n // we were getting the same latestHash upon re-logging in for test \"user2 rejoins groupA\".\n // We add it to the same queue as '/remove' above gets run on so that it is run after\n // contractID is removed. See also comments in 'gi.actions/identity/login'.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/saveOurLoginState'])\n .then(function () {\n const router = sbp('controller/router')\n const switchFrom = router.currentRoute.path\n const switchTo = groupIdToSwitch ? '/dashboard' : '/'\n if (switchFrom !== '/join' && switchFrom !== switchTo) {\n router.push({ path: switchTo }).catch(console.warn)\n }\n }).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown during queueEvent to ${contractID} by saveOurLoginState:`, e)\n })\n // TODO - #828 remove other group members contracts if applicable\n } else {\n const myProfile = getters.groupProfile(username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n const memberRemovedThemselves = data.member === meta.username\n\n sbp('gi.notifications/emit', // emit a notification for a member removal.\n memberRemovedThemselves ? 'MEMBER_LEFT' : 'MEMBER_REMOVED',\n {\n groupID: contractID,\n username: memberRemovedThemselves ? meta.username : data.member\n })\n }\n // TODO - #828 remove the member contract if applicable.\n // problem is, if they're in another group we're also a part of, or if we\n // have a DM with them, we don't want to do this. may need to use manual reference counting\n // sbp('chelonia/contract/release', getters.groupProfile(data.member).contractID)\n }\n // TODO - #850 verify open proposals and see if they need some re-adjustment.\n }\n },\n 'gi.contracts/group/removeOurselves': {\n validate: objectMaybeOf({\n reason: string\n }),\n process ({ data, meta, contractID }, { state, getters }) {\n memberLeaves(\n { username: meta.username, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', {\n meta,\n data: { member: meta.username, reason: data.reason || '' },\n contractID\n }]\n )\n }\n },\n 'gi.contracts/group/invite': {\n validate: inviteType,\n process ({ data, meta }, { state }) {\n Vue.set(state.invites, data.inviteSecret, data)\n }\n },\n 'gi.contracts/group/inviteAccept': {\n validate: objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n }),\n process ({ data, meta }, { state }) {\n console.debug('inviteAccept:', data, state.invites)\n const invite = state.invites[data.inviteSecret]\n if (invite.status !== INVITE_STATUS.VALID) {\n console.error(`inviteAccept: invite for ${meta.username} is: ${invite.status}`)\n return\n }\n Vue.set(invite.responses, meta.username, true)\n if (Object.keys(invite.responses).length === invite.quantity) {\n invite.status = INVITE_STATUS.USED\n }\n // TODO: ensure `meta.username` is unique for the lifetime of the username\n // since we are making it possible for the same username to leave and\n // rejoin the group. All of their past posts will be re-associated with\n // them upon re-joining.\n Vue.set(state.profiles, meta.username, initGroupProfile(meta.identityContractID, meta.createdDate))\n // If we're triggered by handleEvent in state.js (and not latestContractState)\n // then the asynchronous sideEffect function will get called next\n // and we will subscribe to this new user's identity contract\n },\n // !! IMPORANT!!\n // Actions here MUST NOT modify contract state!\n // They MUST NOT call 'commit'!\n // They should only coordinate the actions of outside contracts.\n // Otherwise `latestContractState` and `handleEvent` will not produce same state!\n async sideEffect ({ meta, contractID }, { state }) {\n const { loggedIn } = sbp('state/vuex/state')\n const { profiles = {} } = state\n\n // TODO: per #257 this will ,have to be encompassed in a recoverable transaction\n // however per #610 that might be handled in handleEvent (?), or per #356 might not be needed\n if (meta.username === loggedIn.username) {\n // we're the person who just accepted the group invite\n // so subscribe to founder's IdentityContract & everyone else's\n for (const name in profiles) {\n if (name !== loggedIn.username) {\n await sbp('chelonia/contract/sync', profiles[name].contractID)\n }\n }\n } else {\n const myProfile = profiles[loggedIn.username]\n // we're an existing member of the group getting notified that a\n // new member has joined, so subscribe to their identity contract\n await sbp('chelonia/contract/sync', meta.identityContractID)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'MEMBER_ADDED', { // emit a notification for a member addition.\n groupID: contractID,\n username: meta.username\n })\n }\n }\n }\n },\n 'gi.contracts/group/inviteRevoke': {\n validate: (data, { state, meta }) => {\n objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n })(data)\n\n if (!state.invites[data.inviteSecret]) {\n throw new TypeError(L('The link does not exist.'))\n }\n },\n process ({ data, meta }, { state }) {\n const invite = state.invites[data.inviteSecret]\n Vue.set(invite, 'status', INVITE_STATUS.REVOKED)\n }\n },\n 'gi.contracts/group/updateSettings': {\n // OPTIMIZE: Make this custom validation function\n // reusable accross other future validators\n validate: objectMaybeOf({\n groupName: x => typeof x === 'string',\n groupPicture: x => typeof x === 'string',\n sharedValues: x => typeof x === 'string',\n mincomeAmount: x => typeof x === 'number' && x > 0,\n mincomeCurrency: x => typeof x === 'string'\n }),\n process ({ meta, data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/group/groupProfileUpdate': {\n validate: objectMaybeOf({\n incomeDetailsType: x => ['incomeAmount', 'pledgeAmount'].includes(x),\n incomeAmount: x => typeof x === 'number' && x >= 0,\n pledgeAmount: x => typeof x === 'number' && x >= 0,\n nonMonetaryAdd: string,\n nonMonetaryEdit: objectOf({\n replace: string,\n with: string\n }),\n nonMonetaryRemove: string,\n paymentMethods: arrayOf(\n objectOf({\n name: string,\n value: string\n })\n )\n }),\n process ({ data, meta }, { state, getters }) {\n const groupProfile = state.profiles[meta.username]\n const nonMonetary = groupProfile.nonMonetaryContributions\n for (const key in data) {\n const value = data[key]\n switch (key) {\n case 'nonMonetaryAdd':\n nonMonetary.push(value)\n break\n case 'nonMonetaryRemove':\n nonMonetary.splice(nonMonetary.indexOf(value), 1)\n break\n case 'nonMonetaryEdit':\n nonMonetary.splice(nonMonetary.indexOf(value.replace), 1, value.with)\n break\n default:\n Vue.set(groupProfile, key, value)\n }\n }\n if (data.incomeDetailsType) {\n // someone updated their income details, create a snapshot of the haveNeeds\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n },\n 'gi.contracts/group/updateAllVotingRules': {\n validate: objectMaybeOf({\n ruleName: x => [RULE_PERCENTAGE, RULE_DISAGREEMENT].includes(x),\n ruleThreshold: number,\n expires_ms: number\n }),\n process ({ data, meta }, { state }) {\n // Update all types of proposal settings for simplicity\n if (data.ruleName && data.ruleThreshold) {\n for (const proposalSettings in state.settings.proposals) {\n Vue.set(state.settings.proposals[proposalSettings], 'rule', data.ruleName)\n Vue.set(state.settings.proposals[proposalSettings].ruleSettings[data.ruleName], 'threshold', data.ruleThreshold)\n }\n }\n\n // TODO later - support update expires_ms\n // if (data.ruleName && data.expires_ms) {\n // for (const proposalSetting in state.settings.proposals) {\n // Vue.set(state.settings.proposals[proposalSetting].ruleSettings[data.ruleName], 'expires_ms', data.expires_ms)\n // }\n // }\n }\n },\n 'gi.contracts/group/addChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n attributes: chatRoomAttributesType\n }),\n process ({ data, meta }, { state }) {\n const { name, type, privacyLevel } = data.attributes\n Vue.set(state.chatRooms, data.chatRoomID, {\n creator: meta.username,\n name,\n type,\n privacyLevel,\n deletedDate: null,\n users: []\n })\n if (!state.generalChatRoomId) {\n Vue.set(state, 'generalChatRoomId', data.chatRoomID)\n }\n }\n },\n 'gi.contracts/group/deleteChatRoom': {\n validate: (data, { getters, meta }) => {\n objectOf({ chatRoomID: string })(data)\n\n if (getters.getChatRooms[data.chatRoomID].creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state }) {\n Vue.delete(state.chatRooms, data.chatRoomID)\n }\n },\n 'gi.contracts/group/leaveChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n member: string,\n leavingGroup: boolean // if kicker is exists, it means group leaving\n }),\n process ({ data, meta }, { state }) {\n Vue.set(state.chatRooms[data.chatRoomID], 'users',\n state.chatRooms[data.chatRoomID].users.filter(u => u !== data.member))\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (meta.username === rootState.loggedIn.username && !sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n const sendingData = data.leavingGroup\n ? { member: data.member }\n : { member: data.member, username: meta.username }\n await sbp('gi.actions/chatroom/leave', { contractID: data.chatRoomID, data: sendingData })\n }\n }\n },\n 'gi.contracts/group/joinChatRoom': {\n validate: objectMaybeOf({\n username: string,\n chatRoomID: string\n }),\n process ({ data, meta }, { state }) {\n const username = data.username || meta.username\n state.chatRooms[data.chatRoomID].users.push(username)\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n const username = data.username || meta.username\n if (username === rootState.loggedIn.username) {\n if (!sbp('okTurtles.data/get', 'JOINING_GROUP') || sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n // while users are joining chatroom, they don't need to leave chatrooms\n // this is similar to setting 'JOINING_GROUP' before joining group\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', data.chatRoomID)\n await sbp('chelonia/contract/sync', data.chatRoomID)\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', undefined)\n sbp('okTurtles.data/set', 'READY_TO_JOIN_CHATROOM', false)\n }\n }\n }\n },\n 'gi.contracts/group/renameChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n name: string\n }),\n process ({ data, meta }, { state, getters }) {\n Vue.set(state.chatRooms, data.chatRoomID, {\n ...getters.getChatRooms[data.chatRoomID],\n name: data.name\n })\n }\n },\n ...((process.env.NODE_ENV === 'development' || process.env.CI) && {\n 'gi.contracts/group/forceDistributionDate': {\n validate: optional,\n process ({ meta }, { state, getters }) {\n getters.groupSettings.distributionDate = dateToPeriodStamp(meta.createdDate)\n }\n },\n 'gi.contracts/group/malformedMutation': {\n validate: objectOf({ errorType: string, sideEffect: optional(boolean) }),\n process ({ data }) {\n const ErrorType = Errors[data.errorType]\n if (data.sideEffect) return\n if (ErrorType) {\n throw new ErrorType('malformedMutation!')\n } else {\n throw new Error(`unknown error type: ${data.errorType}`)\n }\n },\n sideEffect (message, { state }) {\n if (!message.data.sideEffect) return\n sbp('gi.contracts/group/malformedMutation/process', {\n ...message,\n data: omit(message.data, ['sideEffect'])\n }, state)\n }\n }\n })\n // TODO: remove group profile when leave group is implemented\n },\n // methods are SBP selectors that are version-tracked for each contract.\n // in other words, you can use them to define SBP selectors that will\n // contain functions that you can modify across different contract versions,\n // and when the contract calls them, it will use that specific version of the\n // method.\n //\n // They are useful when used in conjunction with pushSideEffect from process\n // functions.\n //\n // IMPORTANT: they MUST begin with the name of the contract.\n methods: {\n 'gi.contracts/group/archiveProposal': async function (contractID, proposalHash, proposal) {\n const { username } = sbp('state/vuex/state').loggedIn\n const key = `proposals/${username}/${contractID}`\n const proposals = await sbp('gi.db/archive/load', key) || []\n // newest at the front of the array, oldest at the back\n proposals.unshift([proposalHash, proposal])\n while (proposals.length > MAX_ARCHIVED_PROPOSALS) {\n proposals.pop()\n }\n await sbp('gi.db/archive/save', key, proposals)\n sbp('okTurtles.events/emit', PROPOSAL_ARCHIVED, [proposalHash, proposal])\n }\n }\n})\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj /*: Object */, fn /*: Function */, o /*: Object */ = {}) /*: any */ {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj /*: Object */, fn /*: Function */) /*: {[any]: any} */ {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o /*: Object */, props /*: string[] */) /*: Object */ {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o /*: Object */, where /*: Function */) /*: Object */ {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array /*: Array<*> */, indices /*: Array */) /*: Array<*> */ {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o /*: Object */, props /*: string[] */) /*: {...} */ {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj /*: Object */) /*: any */ {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj /*: Object */, src /*: Object */) /*: any */ {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec /*: number */) /*: Promise */ {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length /*: number */) /*: Uint8Array */ {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length /*: number */) /*: string */ {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min /*: number */, max /*: number */) /*: number */ {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr /*: any[] */) /*: any */ {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr /*: Array<*> */) /*: Array */ {\n let flat /*: Array<*> */ = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () /*: any[] */ {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array /*: any[] */) /*: any[] */ {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a /*: any */, b /*: any */) /*: boolean */ {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func /*: Function */, wait /*: number */, immediate /*: ?boolean */) /*: Function */ {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n\n/**\n * Gets the value at `path` of `obj`. If the resolved value is\n * `undefined`, the `defaultValue` is returned in its place.\n *\n */\nexport function get (obj /*: Object */, path /*: string[] */, defaultValue /*: any */) /*: any */ {\n if (!path.length) {\n return obj\n } else if (obj === undefined) {\n return defaultValue\n }\n\n let result = obj\n let i = 0\n while (result && i < path.length) {\n result = result[path[i]]\n i++\n }\n\n return result === undefined ? defaultValue : result\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nexport class GIErrorIgnoreAndBan extends Error {\n // ugly boilerplate because JavaScript is stupid\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types\n constructor (...params ) {\n super(...params)\n this.name = 'GIErrorIgnoreAndBan'\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n\n// Used to throw human readable errors on UI.\nexport class GIErrorUIRuntimeError extends Error {\n constructor (...params ) {\n super(...params)\n // this.name = this.constructor.name\n this.name = 'GIErrorUIRuntimeError' // string literal so minifier doesn't overwrite\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor)\n }\n }\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal (\n { state, proposalHash, proposal, contractID } \n \n ) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n", "'use strict'\n\nimport { unionOf, literalOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nexport const PAYMENT_PENDING = 'pending'\nexport const PAYMENT_CANCELLED = 'cancelled'\nexport const PAYMENT_ERROR = 'error'\nexport const PAYMENT_NOT_RECEIVED = 'not-received'\nexport const PAYMENT_COMPLETED = 'completed'\nexport const paymentStatusType = unionOf(...[PAYMENT_PENDING, PAYMENT_CANCELLED, PAYMENT_ERROR, PAYMENT_NOT_RECEIVED, PAYMENT_COMPLETED].map(k => literalOf(k)))\nexport const PAYMENT_TYPE_MANUAL = 'manual'\nexport const PAYMENT_TYPE_BITCOIN = 'bitcoin'\nexport const PAYMENT_TYPE_PAYPAL = 'paypal'\nexport const paymentType = unionOf(...[PAYMENT_TYPE_MANUAL, PAYMENT_TYPE_BITCOIN, PAYMENT_TYPE_PAYPAL].map(k => literalOf(k)))\n", "'use strict'\n\n \n \n \n \n\nexport default function mincomeProportional (haveNeeds ) {\n let totalHave = 0\n let totalNeed = 0\n const havers = []\n const needers = []\n for (const haveNeed of haveNeeds) {\n if (haveNeed.haveNeed > 0) {\n havers.push(haveNeed)\n totalHave += haveNeed.haveNeed\n } else if (haveNeed.haveNeed < 0) {\n needers.push(haveNeed)\n totalNeed += Math.abs(haveNeed.haveNeed)\n }\n }\n const totalPercent = Math.min(1, totalNeed / totalHave)\n const payments = []\n for (const haver of havers) {\n const distributionAmount = totalPercent * haver.haveNeed\n for (const needer of needers) {\n const belowPercentage = Math.abs(needer.haveNeed) / totalNeed\n payments.push({\n amount: distributionAmount * belowPercentage,\n from: haver.name,\n to: needer.name\n })\n }\n }\n return payments\n}\n", "'use strict'\n\n// greedy algorithm responsible for \"balancing\" payments\n// such that the least number of payments are made.\nexport default function minimizeTotalPaymentsCount (\n distribution \n) {\n const neederTotalReceived = {}\n const haverTotalHave = {}\n const haversSorted = []\n const needersSorted = []\n const minimizedDistribution = []\n for (const todo of distribution) {\n neederTotalReceived[todo.to] = (neederTotalReceived[todo.to] || 0) + todo.amount\n haverTotalHave[todo.from] = (haverTotalHave[todo.from] || 0) + todo.amount\n }\n for (const name in haverTotalHave) {\n haversSorted.push({ name, amount: haverTotalHave[name] })\n }\n for (const name in neederTotalReceived) {\n needersSorted.push({ name, amount: neederTotalReceived[name] })\n }\n // sort haves and needs: greatest to least\n haversSorted.sort((a, b) => b.amount - a.amount)\n needersSorted.sort((a, b) => b.amount - a.amount)\n while (haversSorted.length > 0 && needersSorted.length > 0) {\n const mostHaver = haversSorted.pop()\n const mostNeeder = needersSorted.pop()\n const diff = mostHaver.amount - mostNeeder.amount\n if (diff < 0) {\n // we used up everything the haver had\n minimizedDistribution.push({ amount: mostHaver.amount, from: mostHaver.name, to: mostNeeder.name })\n mostNeeder.amount -= mostHaver.amount\n needersSorted.push(mostNeeder)\n } else if (diff > 0) {\n // we completely filled up the needer's need and still have some left over\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n mostHaver.amount -= mostNeeder.amount\n haversSorted.push(mostHaver)\n } else {\n // a perfect match\n minimizedDistribution.push({ amount: mostNeeder.amount, from: mostHaver.name, to: mostNeeder.name })\n }\n }\n return minimizedDistribution\n}\n", "'use strict'\n\n \n \n \n \n \n \n \n \n\n// https://github.com/okTurtles/group-income/issues/813#issuecomment-593680834\n// round all accounting to DECIMALS_MAX decimal places max to avoid consensus\n// issues that can arise due to different floating point values\n// at extreme precisions. If this becomes inadequate, instead of increasing\n// this value, switch to a different currency base, e.g. from BTC to mBTC.\nexport const DECIMALS_MAX = 8\n\nfunction commaToDots (value ) {\n // ex: \"1,55\" -> \"1.55\"\n return typeof value === 'string' ? value.replace(/,/, '.') : value.toString()\n}\n\nfunction isNumeric (nr ) {\n return !isNaN((nr ) - parseFloat(nr))\n}\n\nfunction isInDecimalsLimit (nr , decimalsMax ) {\n const decimals = nr.split('.')[1]\n return !decimals || decimals.length <= decimalsMax\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nfunction validateMincome (value , decimalsMax ) {\n const nr = commaToDots(value)\n return isNumeric(nr) && isInDecimalsLimit(nr, decimalsMax)\n}\n\nfunction decimalsOrInt (num , decimalsMax ) {\n // ex: 12.5 -> \"12.50\", but 250 -> \"250\"\n return num.toFixed(decimalsMax).replace(/\\.0+$/, '')\n}\n\nexport function saferFloat (value ) {\n // ex: 1.333333333333333333 -> 1.33333333\n return parseFloat(value.toFixed(DECIMALS_MAX))\n}\n\nexport function normalizeCurrency (value ) {\n // ex: \"1,333333333333333333\" -> 1.33333333\n return saferFloat(parseFloat(commaToDots(value)))\n}\n\n// NOTE: Unsure whether this is *always* string; it comes from 'validators' in other files\nexport function mincomePositive (value ) {\n return parseFloat(commaToDots(value)) > 0\n}\n\nfunction makeCurrency (options) {\n const { symbol, symbolWithCode, decimalsMax, formatCurrency } = options\n return {\n symbol,\n symbolWithCode,\n decimalsMax,\n displayWithCurrency: (n ) => formatCurrency(decimalsOrInt(n, decimalsMax)),\n displayWithoutCurrency: (n ) => decimalsOrInt(n, decimalsMax),\n validate: (n ) => validateMincome(n, decimalsMax)\n }\n}\n\n// NOTE: if we needed for some reason, this could also be defined in\n// a json file that's read in and generates this object. For\n// example, that would allow the addition of currencies without\n// having to \"recompile\" a new version of the app.\nconst currencies = {\n USD: makeCurrency({\n symbol: '$',\n symbolWithCode: '$ USD',\n decimalsMax: 2,\n formatCurrency: amount => '$' + amount\n }),\n EUR: makeCurrency({\n symbol: '\u20AC',\n symbolWithCode: '\u20AC EUR',\n decimalsMax: 2,\n formatCurrency: amount => '\u20AC' + amount\n }),\n BTC: makeCurrency({\n symbol: '\u0243',\n symbolWithCode: '\u0243 BTC',\n decimalsMax: DECIMALS_MAX,\n formatCurrency: amount => amount + '\u0243'\n })\n}\n\nexport default currencies\n", "'use strict'\n\nimport mincomeProportional from './mincome-proportional.js'\nimport minimizeTotalPaymentsCount from './payments-minimizer.js'\nimport { cloneDeep } from '../giLodash.js'\nimport { saferFloat, DECIMALS_MAX } from '../currencies.js'\n\n \n\nconst tinyNum = 1 / Math.pow(10, DECIMALS_MAX)\n\nexport function unadjustedDistribution ({ haveNeeds = [], minimize = true } \n \n ) {\n const distribution = mincomeProportional(haveNeeds)\n return minimize ? minimizeTotalPaymentsCount(distribution) : distribution\n}\n\nexport function adjustedDistribution (\n { distribution, payments, dueOn } \n) {\n distribution = cloneDeep(distribution)\n // ensure the total is set because of how reduceDistribution works\n for (const todo of distribution) {\n todo.total = todo.amount\n }\n distribution = subtractDistributions(distribution, payments)\n // remove any todos for containing miniscule amounts\n // and pledgers who switched sides should have their todos removed\n .filter(todo => todo.amount >= tinyNum)\n for (const todo of distribution) {\n todo.amount = saferFloat(todo.amount)\n todo.total = saferFloat(todo.total)\n todo.partial = todo.total !== todo.amount\n todo.isLate = false\n todo.dueOn = dueOn\n }\n // TODO: add in latePayments to the end of the distribution\n // consider passing in latePayments\n return distribution\n}\n\n// Merges multiple payments between any combinations two of users:\nfunction reduceDistribution (payments ) {\n // Don't modify the payments list/object parameter in-place, as this is not intended:\n payments = cloneDeep(payments)\n for (let i = 0; i < payments.length; i++) {\n const paymentA = payments[i]\n for (let j = i + 1; j < payments.length; j++) {\n const paymentB = payments[j]\n\n // Were paymentA and paymentB between the same two users?\n if ((paymentA.from === paymentB.from && paymentA.to === paymentB.to) ||\n (paymentA.to === paymentB.from && paymentA.from === paymentB.to)) {\n // Add or subtract paymentB's amount to paymentA's amount, depending on the relative\n // direction of the two payments:\n paymentA.amount += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.amount\n paymentA.total += (paymentA.from === paymentB.from ? 1 : -1) * paymentB.total\n // Remove paymentB from payments, and decrement the inner sentinal loop variable:\n payments.splice(j, 1)\n j--\n }\n }\n }\n return payments\n}\n\nfunction addDistributions (paymentsA , paymentsB ) {\n return reduceDistribution([...paymentsA, ...paymentsB])\n}\n\nfunction subtractDistributions (paymentsA , paymentsB ) {\n // Don't modify any payment list/objects parameters in-place, as this is not intended:\n paymentsB = cloneDeep(paymentsB)\n // Reverse the sign of the second operand's amounts so that the final addition is actually subtraction:\n for (const p of paymentsB) {\n p.amount *= -1\n p.total *= -1\n }\n return addDistributions(paymentsA, paymentsB)\n}\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue, Errors, L } from '@common/common.js'\nimport votingRules, { ruleType, VOTE_FOR, VOTE_AGAINST, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './shared/voting/rules.js'\nimport proposals, { proposalType, proposalSettingsType, archiveProposal } from './shared/voting/proposals.js'\nimport {\n PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC, STATUS_OPEN, STATUS_CANCELLED, MAX_ARCHIVED_PROPOSALS, PROPOSAL_ARCHIVED,\n INVITE_INITIAL_CREATOR, INVITE_STATUS, PROFILE_STATUS, INVITE_EXPIRES_IN_DAYS\n} from './shared/constants.js'\nimport { paymentStatusType, paymentType, PAYMENT_COMPLETED } from './shared/payments/index.js'\nimport { merge, deepEqualJSONType, omit } from './shared/giLodash.js'\nimport { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, periodStampGivenDate, dateIsWithinPeriod, DAYS_MILLIS } from './shared/time.js'\nimport { unadjustedDistribution, adjustedDistribution } from './shared/distribution/distribution.js'\nimport currencies, { saferFloat } from './shared/currencies.js'\nimport { inviteType, chatRoomAttributesType } from './shared/types.js'\nimport { arrayOf, mapOf, objectOf, objectMaybeOf, optional, string, number, boolean, object, unionOf, tupleOf } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nfunction vueFetchInitKV (obj , key , initialValue ) {\n let value = obj[key]\n if (!value) {\n Vue.set(obj, key, initialValue)\n value = obj[key]\n }\n return value\n}\n\nfunction initGroupProfile (contractID , joinedDate ) {\n return {\n globalUsername: '', // TODO: this? e.g. groupincome:greg / namecoin:bob / ens:alice\n contractID,\n joinedDate,\n nonMonetaryContributions: [],\n status: PROFILE_STATUS.ACTIVE,\n departedDate: null\n }\n}\n\nfunction initPaymentPeriod ({ getters }) {\n return {\n // this saved so that it can be used when creating a new payment\n initialCurrency: getters.groupMincomeCurrency,\n // TODO: should we also save the first period's currency exchange rate..?\n // all payments during the period use this to set their exchangeRate\n // see notes and code in groupIncomeAdjustedDistribution for details.\n // TODO: for the currency change proposal, have it update the mincomeExchangeRate\n // using .mincomeExchangeRate *= proposal.exchangeRate\n mincomeExchangeRate: 1, // modified by proposals to change mincome currency\n paymentsFrom: {}, // fromUser => toUser => Array\n // snapshot of adjusted distribution after each completed payment\n // yes, it is possible a payment began in one period and completed in another\n // in which case lastAdjustedDistribution for the previous period will be updated\n lastAdjustedDistribution: null,\n // snapshot of haveNeeds. made only when there are no payments\n haveNeedsSnapshot: null\n }\n}\n\n// NOTE: do not call any of these helper functions from within a getter b/c they modify state!\n\nfunction clearOldPayments ({ state, getters }) {\n const sortedPeriodKeys = Object.keys(state.paymentsByPeriod).sort()\n // save two periods worth of payments, max\n while (sortedPeriodKeys.length > 2) {\n const period = sortedPeriodKeys.shift()\n for (const paymentHash of getters.paymentHashesForPeriod(period)) {\n Vue.delete(state.payments, paymentHash)\n // TODO: archive the old payments in a sideEffect, not here\n }\n Vue.delete(state.paymentsByPeriod, period)\n }\n}\n\nfunction initFetchPeriodPayments ({ meta, state, getters }) {\n const period = getters.periodStampGivenDate(meta.createdDate)\n const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ getters }))\n clearOldPayments({ state, getters })\n return periodPayments\n}\n\n// this function is called each time a payment is completed or a user adjusts their income details.\n// TODO: call also when mincome is adjusted\nfunction updateCurrentDistribution ({ meta, state, getters }) {\n const curPeriodPayments = initFetchPeriodPayments({ meta, state, getters })\n const period = getters.periodStampGivenDate(meta.createdDate)\n const noPayments = Object.keys(curPeriodPayments.paymentsFrom).length === 0\n // update distributionDate if we've passed into the next period\n if (comparePeriodStamps(period, getters.groupSettings.distributionDate) > 0) {\n getters.groupSettings.distributionDate = period\n }\n // save haveNeeds if there are no payments or the haveNeeds haven't been saved yet\n if (noPayments || !curPeriodPayments.haveNeedsSnapshot) {\n curPeriodPayments.haveNeedsSnapshot = getters.haveNeedsForThisPeriod(period)\n }\n // if there are payments this period, save the adjusted distribution\n if (!noPayments) {\n updateAdjustedDistribution({ period, getters })\n }\n}\n\nfunction updateAdjustedDistribution ({ period, getters }) {\n const payments = getters.groupPeriodPayments[period]\n if (payments && payments.haveNeedsSnapshot) {\n const minimize = getters.groupSettings.minimizeDistribution\n payments.lastAdjustedDistribution = adjustedDistribution({\n distribution: unadjustedDistribution({ haveNeeds: payments.haveNeedsSnapshot, minimize }),\n payments: getters.paymentsForPeriod(period),\n dueOn: getters.dueDateForPeriod(period)\n }).filter(todo => {\n // only return todos for active members\n return getters.groupProfile(todo.to).status === PROFILE_STATUS.ACTIVE\n })\n }\n}\n\nfunction memberLeaves ({ username, dateLeft }, { meta, state, getters }) {\n state.profiles[username].status = PROFILE_STATUS.REMOVED\n state.profiles[username].departedDate = dateLeft\n // remove any todos for this member from the adjusted distribution\n updateCurrentDistribution({ meta, state, getters })\n}\n\nfunction isActionYoungerThanUser (actionMeta , userProfile ) {\n // A util function that checks if an action (or event) in a group occurred after a particular user joined a group.\n // This is used mostly for checking if a notification should be sent for that user or not.\n // e.g.) user-2 who joined a group later than user-1 (who is the creator of the group) doesn't need to receive\n // 'MEMBER_ADDED' notification for user-1.\n // In some situations, userProfile is undefined, for example, when inviteAccept is called in\n // certain situations. So we need to check for that here.\n if (!userProfile) {\n return false\n }\n return compareISOTimestamps(actionMeta.createdDate, userProfile.joinedDate) > 0\n}\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/group',\n metadata: {\n validate: objectOf({\n createdDate: string,\n username: string,\n identityContractID: string\n }),\n create () {\n const { username, identityContractID } = sbp('state/vuex/state').loggedIn\n return {\n // TODO: We may want to get the time from the server instead of relying on\n // the client in case the client's clock isn't set correctly.\n // the only issue here is that it involves an async function...\n // See: https://github.com/okTurtles/group-income/issues/531\n createdDate: new Date().toISOString(),\n username,\n identityContractID\n }\n }\n },\n // These getters are restricted only to the contract's state.\n // Do not access state outside the contract state inside of them.\n // For example, if the getter you use tries to access `state.loggedIn`,\n // that will break the `latestContractState` function in state.js.\n // It is only safe to access state outside of the contract in a contract action's\n // `sideEffect` function (as long as it doesn't modify contract state)\n getters: {\n // we define `currentGroupState` here so that we can redefine it in state.js\n // so that we can re-use these getter definitions in state.js since they are\n // compatible with Vuex getter definitions.\n // Here `state` refers to the individual group contract's state, the equivalent\n // of `vuexRootState[someGroupContractID]`.\n currentGroupState (state) {\n return state\n },\n groupSettings (state, getters) {\n return getters.currentGroupState.settings || {}\n },\n groupProfile (state, getters) {\n return username => {\n const profiles = getters.currentGroupState.profiles\n return profiles && profiles[username]\n }\n },\n groupProfiles (state, getters) {\n const profiles = {}\n for (const username in (getters.currentGroupState.profiles || {})) {\n const profile = getters.groupProfile(username)\n if (profile.status === PROFILE_STATUS.ACTIVE) {\n profiles[username] = profile\n }\n }\n return profiles\n },\n groupMincomeAmount (state, getters) {\n return getters.groupSettings.mincomeAmount\n },\n groupMincomeCurrency (state, getters) {\n return getters.groupSettings.mincomeCurrency\n },\n periodStampGivenDate (state, getters) {\n return (recentDate ) => {\n if (typeof recentDate !== 'string') {\n recentDate = recentDate.toISOString()\n }\n const { distributionDate, distributionPeriodLength } = getters.groupSettings\n return periodStampGivenDate({\n recentDate,\n periodStart: distributionDate,\n periodLength: distributionPeriodLength\n })\n }\n },\n periodBeforePeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -len))\n }\n },\n periodAfterPeriod (state, getters) {\n return (periodStamp ) => {\n const len = getters.groupSettings.distributionPeriodLength\n return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), len))\n }\n },\n dueDateForPeriod (state, getters) {\n return (periodStamp ) => {\n return dateToPeriodStamp(\n addTimeToDate(\n dateFromPeriodStamp(getters.periodAfterPeriod(periodStamp)),\n -DAYS_MILLIS\n )\n )\n }\n },\n paymentTotalFromUserToUser (state, getters) {\n return (fromUser, toUser, periodStamp) => {\n const payments = getters.currentGroupState.payments\n const periodPayments = getters.groupPeriodPayments\n const { paymentsFrom, mincomeExchangeRate } = periodPayments[periodStamp] || {}\n // NOTE: @babel/plugin-proposal-optional-chaining would come in super-handy\n // here, but I couldn't get it to work with our linter. :(\n // https://github.com/babel/babel-eslint/issues/511\n const total = (((paymentsFrom || {})[fromUser] || {})[toUser] || []).reduce((a, hash) => {\n const payment = payments[hash]\n let { amount, exchangeRate, status } = payment.data\n if (status !== PAYMENT_COMPLETED) {\n return a\n }\n const paymentCreatedPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n // if this payment is from a previous period, then make sure to take into account\n // any proposals that passed in between the payment creation and the payment\n // completion that modified the group currency by multiplying both period's\n // exchange rates\n if (periodStamp !== paymentCreatedPeriodStamp) {\n if (paymentCreatedPeriodStamp !== getters.periodBeforePeriod(periodStamp)) {\n console.warn(`paymentTotalFromUserToUser: super old payment shouldn't exist, ignoring! (curPeriod=${periodStamp})`, JSON.stringify(payment))\n return a\n }\n exchangeRate *= periodPayments[paymentCreatedPeriodStamp].mincomeExchangeRate\n }\n return a + (amount * exchangeRate * mincomeExchangeRate)\n }, 0)\n return saferFloat(total)\n }\n },\n paymentHashesForPeriod (state, getters) {\n return (periodStamp) => {\n const periodPayments = getters.groupPeriodPayments[periodStamp]\n if (periodPayments) {\n let hashes = []\n const { paymentsFrom } = periodPayments\n for (const fromUser in paymentsFrom) {\n for (const toUser in paymentsFrom[fromUser]) {\n hashes = hashes.concat(paymentsFrom[fromUser][toUser])\n }\n }\n return hashes\n }\n }\n },\n groupMembersByUsername (state, getters) {\n return Object.keys(getters.groupProfiles)\n },\n groupMembersCount (state, getters) {\n return getters.groupMembersByUsername.length\n },\n groupMembersPending (state, getters) {\n const invites = getters.currentGroupState.invites\n const pendingMembers = {}\n for (const inviteId in invites) {\n const invite = invites[inviteId]\n if (\n invite.status === INVITE_STATUS.VALID &&\n invite.creator !== INVITE_INITIAL_CREATOR\n ) {\n pendingMembers[invites[inviteId].invitee] = {\n invitedBy: invites[inviteId].creator,\n expires: invite.expires\n }\n }\n }\n return pendingMembers\n },\n groupShouldPropose (state, getters) {\n return getters.groupMembersCount >= 3\n },\n groupProposalSettings (state, getters) {\n return (proposalType = PROPOSAL_GENERIC) => {\n return getters.groupSettings.proposals[proposalType]\n }\n },\n groupCurrency (state, getters) {\n const mincomeCurrency = getters.groupMincomeCurrency\n return mincomeCurrency && currencies[mincomeCurrency]\n },\n groupMincomeFormatted (state, getters) {\n return getters.withGroupCurrency?.(getters.groupMincomeAmount)\n },\n groupMincomeSymbolWithCode (state, getters) {\n return getters.groupCurrency?.symbolWithCode\n },\n groupPeriodPayments (state, getters) {\n // note: a lot of code expects this to return an object, so keep the || {} below\n return getters.currentGroupState.paymentsByPeriod || {}\n },\n groupThankYousFrom (state, getters) {\n return getters.currentGroupState.thankYousFrom || {}\n },\n withGroupCurrency (state, getters) {\n // TODO: If this group has no defined mincome currency, not even a default one like\n // USD, then calling this function is probably an error which should be reported.\n // Just make sure the UI doesn't break if an exception is thrown, since this is\n // bound to the UI in some location.\n return getters.groupCurrency?.displayWithCurrency\n },\n getChatRooms (state, getters) {\n return getters.currentGroupState.chatRooms\n },\n generalChatRoomId (state, getters) {\n return getters.currentGroupState.generalChatRoomId\n },\n // getter is named haveNeedsForThisPeriod instead of haveNeedsForPeriod because it uses\n // getters.groupProfiles - and that is always based on the most recent values. we still\n // pass in the current period because it's used to set the \"when\" property\n haveNeedsForThisPeriod (state, getters) {\n return (currentPeriod ) => {\n // NOTE: if we ever switch back to the \"real-time\" adjusted distribution algorithm,\n // make sure that this function also handles userExitsGroupEvent\n const groupProfiles = getters.groupProfiles // TODO: these should use the haveNeeds for the specific period's distribution period\n const haveNeeds = []\n for (const username in groupProfiles) {\n const { incomeDetailsType, joinedDate } = groupProfiles[username]\n if (incomeDetailsType) {\n const amount = groupProfiles[username][incomeDetailsType]\n const haveNeed = incomeDetailsType === 'incomeAmount' ? amount - getters.groupMincomeAmount : amount\n // construct 'when' this way in case we ever use a pro-rated algorithm\n let when = dateFromPeriodStamp(currentPeriod).toISOString()\n if (dateIsWithinPeriod({\n date: joinedDate,\n periodStart: currentPeriod,\n periodLength: getters.groupSettings.distributionPeriodLength\n })) {\n when = joinedDate\n }\n haveNeeds.push({ name: username, haveNeed, when })\n }\n }\n return haveNeeds\n }\n },\n paymentsForPeriod (state, getters) {\n return (periodStamp) => {\n const hashes = getters.paymentHashesForPeriod(periodStamp)\n const events = []\n if (hashes && hashes.length > 0) {\n const payments = getters.currentGroupState.payments\n for (const paymentHash of hashes) {\n const payment = payments[paymentHash]\n if (payment.data.status === PAYMENT_COMPLETED) {\n events.push({\n from: payment.meta.username,\n to: payment.data.toUser,\n hash: paymentHash,\n amount: payment.data.amount,\n isLate: !!payment.data.isLate,\n when: payment.data.completedDate\n })\n }\n }\n }\n return events\n }\n }\n // distributionEventsForMonth (state, getters) {\n // return (monthstamp) => {\n // // NOTE: if we ever switch back to the \"real-time\" adjusted distribution\n // // algorithm, make sure that this function also handles userExitsGroupEvent\n // const distributionEvents = getters.haveNeedEventsForMonth(monthstamp)\n // const paymentEvents = getters.paymentEventsForMonth(monthstamp)\n // distributionEvents.splice(distributionEvents.length, 0, paymentEvents)\n // return distributionEvents.sort((a, b) => compareISOTimestamps(a.data.when, b.data.when))\n // }\n // }\n },\n // NOTE: All mutations must be atomic in their edits of the contract state.\n // THEY ARE NOT to farm out any further mutations through the async actions!\n actions: {\n // this is the constructor\n 'gi.contracts/group': {\n validate: objectMaybeOf({\n invites: mapOf(string, inviteType),\n settings: objectMaybeOf({\n // TODO: add 'groupPubkey'\n groupName: string,\n groupPicture: string,\n sharedValues: string,\n mincomeAmount: number,\n mincomeCurrency: string,\n distributionDate: isPeriodStamp,\n distributionPeriodLength: number,\n minimizeDistribution: boolean,\n proposals: objectOf({\n [PROPOSAL_INVITE_MEMBER]: proposalSettingsType,\n [PROPOSAL_REMOVE_MEMBER]: proposalSettingsType,\n [PROPOSAL_GROUP_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: proposalSettingsType,\n [PROPOSAL_GENERIC]: proposalSettingsType\n })\n })\n }),\n process ({ data, meta }, { state, getters }) {\n // TODO: checkpointing: https://github.com/okTurtles/group-income/issues/354\n const initialState = merge({\n payments: {},\n paymentsByPeriod: {},\n thankYousFrom: {}, // { fromUser1: { toUser1: msg1, toUser2: msg2, ... }, fromUser2: {}, ... }\n invites: {},\n proposals: {}, // hashes => {} TODO: this, see related TODOs in GroupProposal\n settings: {\n groupCreator: meta.username,\n distributionPeriodLength: 30 * DAYS_MILLIS,\n inviteExpiryOnboarding: INVITE_EXPIRES_IN_DAYS.ON_BOARDING,\n inviteExpiryProposal: INVITE_EXPIRES_IN_DAYS.PROPOSAL\n },\n profiles: {\n [meta.username]: initGroupProfile(meta.identityContractID, meta.createdDate)\n },\n chatRooms: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n initFetchPeriodPayments({ meta, state, getters })\n }\n },\n 'gi.contracts/group/payment': {\n validate: objectMaybeOf({\n // TODO: how to handle donations to okTurtles?\n // TODO: how to handle payments to groups or users outside of this group?\n toUser: string,\n amount: number,\n currencyFromTo: tupleOf(string, string), // must be one of the keys in currencies.js (e.g. USD, EUR, etc.) TODO: handle old clients not having one of these keys, see OP_PROTOCOL_UPGRADE https://github.com/okTurtles/group-income/issues/603\n // multiply 'amount' by 'exchangeRate', which must always be\n // based on the initialCurrency of the period in which this payment was created.\n // it is then further multiplied by the period's 'mincomeExchangeRate', which\n // is modified if any proposals pass to change the mincomeCurrency\n exchangeRate: number,\n txid: string,\n status: paymentStatusType,\n paymentType: paymentType,\n details: optional(object),\n memo: optional(string)\n }),\n process ({ data, meta, hash }, { state, getters }) {\n if (data.status === PAYMENT_COMPLETED) {\n console.error(`payment: payment ${hash} cannot have status = 'completed'!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('payments cannot be instantly completed!')\n }\n Vue.set(state.payments, hash, {\n data: {\n ...data,\n groupMincome: getters.groupMincomeAmount\n },\n meta,\n history: [[meta.createdDate, hash]]\n })\n const { paymentsFrom } = initFetchPeriodPayments({ meta, state, getters })\n const fromUser = vueFetchInitKV(paymentsFrom, meta.username, {})\n const toUser = vueFetchInitKV(fromUser, data.toUser, [])\n toUser.push(hash)\n // TODO: handle completed payments here too! (for manual payment support)\n }\n },\n 'gi.contracts/group/paymentUpdate': {\n validate: objectMaybeOf({\n paymentHash: string,\n updatedProperties: objectMaybeOf({\n status: paymentStatusType,\n details: object,\n memo: string\n })\n }),\n process ({ data, meta, hash }, { state, getters }) {\n // TODO: we don't want to keep a history of all payments in memory all the time\n // https://github.com/okTurtles/group-income/issues/426\n const payment = state.payments[data.paymentHash]\n // TODO: move these types of validation errors into the validate function so\n // that they can be done before sending as well as upon receiving\n if (!payment) {\n console.error(`paymentUpdate: no payment ${data.paymentHash}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate without existing payment')\n }\n // if the payment is being modified by someone other than the person who sent or received it, throw an exception\n if (meta.username !== payment.meta.username && meta.username !== payment.data.toUser) {\n console.error(`paymentUpdate: bad username ${meta.username} != ${payment.meta.username} != ${payment.data.username}`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('paymentUpdate from bad user!')\n }\n payment.history.push([meta.createdDate, hash])\n merge(payment.data, data.updatedProperties)\n // we update \"this period\"'s snapshot 'lastAdjustedDistribution' on each completed payment\n if (data.updatedProperties.status === PAYMENT_COMPLETED) {\n payment.data.completedDate = meta.createdDate\n // update the current distribution unless this update is for a payment from the previous period\n const updatePeriodStamp = getters.periodStampGivenDate(meta.createdDate)\n const paymentPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate)\n if (comparePeriodStamps(updatePeriodStamp, paymentPeriodStamp) > 0) {\n updateAdjustedDistribution({ period: paymentPeriodStamp, getters })\n } else {\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n },\n sideEffect ({ meta, contractID, data }, { state, getters }) {\n if (data.updatedProperties.status === PAYMENT_COMPLETED) {\n const { loggedIn } = sbp('state/vuex/state')\n const payment = state.payments[data.paymentHash]\n\n if (loggedIn.username === payment.data.toUser) {\n sbp('gi.notifications/emit', 'PAYMENT_RECEIVED', {\n groupID: contractID,\n creator: meta.username,\n paymentHash: data.paymentHash,\n amount: getters.withGroupCurrency(payment.data.amount)\n })\n }\n }\n }\n },\n 'gi.contracts/group/sendPaymentThankYou': {\n validate: objectOf({\n fromUser: string,\n toUser: string,\n memo: string\n }),\n process ({ data }, { state }) {\n const fromUser = vueFetchInitKV(state.thankYousFrom, data.fromUser, {})\n Vue.set(fromUser, data.toUser, data.memo)\n },\n sideEffect ({ contractID, meta, data }) {\n const { loggedIn } = sbp('state/vuex/state')\n\n if (data.toUser === loggedIn.username) {\n sbp('gi.notifications/emit', 'PAYMENT_THANKYOU_SENT', {\n groupID: contractID,\n creator: meta.username, // username of the from user. to be used with sbp('namespace/lookup') in 'AvatarUser.vue'\n fromUser: data.fromUser, // display name of the from user\n toUser: data.toUser\n })\n }\n }\n },\n 'gi.contracts/group/proposal': {\n validate: (data, { state, meta }) => {\n objectOf({\n proposalType: proposalType,\n proposalData: object, // data for Vue widgets\n votingRule: ruleType,\n expires_date_ms: number // calculate by grabbing proposal expiry from group properties and add to `meta.createdDate`\n })(data)\n\n const dataToCompare = omit(data.proposalData, ['reason'])\n\n // Validate this isn't a duplicate proposal\n for (const hash in state.proposals) {\n const prop = state.proposals[hash]\n if (prop.status !== STATUS_OPEN || prop.data.proposalType !== data.proposalType) {\n continue\n }\n\n if (deepEqualJSONType(omit(prop.data.proposalData, ['reason']), dataToCompare)) {\n throw new TypeError(L('There is an identical open proposal.'))\n }\n\n // TODO - verify if type of proposal already exists (SETTING_CHANGE).\n }\n },\n process ({ data, meta, hash }, { state }) {\n Vue.set(state.proposals, hash, {\n data,\n meta,\n votes: { [meta.username]: VOTE_FOR },\n status: STATUS_OPEN,\n payload: null // set later by group/proposalVote\n })\n // TODO: save all proposals disk so that we only keep open proposals in memory\n // TODO: create a global timer to auto-pass/archive expired votes\n // make sure to set that proposal's status as STATUS_EXPIRED if it's expired\n },\n sideEffect ({ contractID, meta, data }, { getters }) {\n const { loggedIn } = sbp('state/vuex/state')\n const typeToSubTypeMap = {\n [PROPOSAL_INVITE_MEMBER]: 'ADD_MEMBER',\n [PROPOSAL_REMOVE_MEMBER]: 'REMOVE_MEMBER',\n [PROPOSAL_GROUP_SETTING_CHANGE]: 'CHANGE_MINCOME',\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: 'CHANGE_VOTING_RULE',\n [PROPOSAL_GENERIC]: 'GENERIC'\n }\n\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'NEW_PROPOSAL', {\n groupID: contractID,\n creator: meta.username,\n subtype: typeToSubTypeMap[data.proposalType]\n })\n }\n }\n },\n 'gi.contracts/group/proposalVote': {\n validate: objectOf({\n proposalHash: string,\n vote: string,\n passPayload: optional(unionOf(object, string)) // TODO: this, somehow we need to send an OP_KEY_ADD GIMessage to add a generated once-only writeonly message public key to the contract, and (encrypted) include the corresponding invite link, also, we need all clients to verify that this message/operation was valid to prevent a hacked client from adding arbitrary OP_KEY_ADD messages, and automatically ban anyone generating such messages\n }),\n process (message, { state }) {\n const { data, hash, meta } = message\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalVote: no proposal for ${data.proposalHash}!`, { data, meta, hash })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n }\n Vue.set(proposal.votes, meta.username, data.vote)\n // TODO: handle vote pass/fail\n // check if proposal is expired, if so, ignore (but log vote)\n if (new Date(meta.createdDate).getTime() > proposal.data.expires_date_ms) {\n console.warn('proposalVote: vote on expired proposal!', { proposal, data, meta })\n // TODO: display warning or something\n return\n }\n // see if this is a deciding vote\n const result = votingRules[proposal.data.votingRule](state, proposal.data.proposalType, proposal.votes)\n if (result === VOTE_FOR || result === VOTE_AGAINST) {\n // handles proposal pass or fail, will update proposal.status accordingly\n proposals[proposal.data.proposalType][result](state, message)\n Vue.set(proposal, 'dateClosed', meta.createdDate)\n }\n },\n sideEffect ({ contractID, data, meta }, { state, getters }) {\n const proposal = state.proposals[data.proposalHash]\n const { loggedIn } = sbp('state/vuex/state')\n const myProfile = getters.groupProfile(loggedIn.username)\n\n if (proposal?.dateClosed &&\n isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'PROPOSAL_CLOSED', {\n groupID: contractID,\n creator: meta.username,\n proposalStatus: proposal.status\n })\n }\n }\n },\n 'gi.contracts/group/proposalCancel': {\n validate: objectOf({\n proposalHash: string\n }),\n process ({ data, meta, contractID }, { state }) {\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // https://github.com/okTurtles/group-income/issues/602\n console.error(`proposalCancel: no proposal for ${data.proposalHash}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalVote without existing proposal')\n } else if (proposal.meta.username !== meta.username) {\n console.error(`proposalCancel: proposal ${data.proposalHash} belongs to ${proposal.meta.username} not ${meta.username}!`, { data, meta })\n throw new Errors.GIErrorIgnoreAndBan('proposalWithdraw for wrong user!')\n }\n Vue.set(proposal, 'status', STATUS_CANCELLED)\n archiveProposal({ state, proposalHash: data.proposalHash, proposal, contractID })\n }\n },\n 'gi.contracts/group/removeMember': {\n validate: (data, { state, getters, meta }) => {\n objectOf({\n member: string, // username to remove\n reason: optional(string),\n automated: optional(boolean),\n // In case it happens in a big group (by proposal)\n // we need to validate the associated proposal.\n proposalHash: optional(string),\n proposalPayload: optional(objectOf({\n secret: string // NOTE: simulate the OP_KEY_* stuff for now\n }))\n })(data)\n\n const memberToRemove = data.member\n const membersCount = getters.groupMembersCount\n\n if (!state.profiles[memberToRemove]) {\n throw new TypeError(L('Not part of the group.'))\n }\n if (membersCount === 1 || memberToRemove === meta.username) {\n throw new TypeError(L('Cannot remove yourself.'))\n }\n\n if (membersCount < 3) {\n // In a small group only the creator can remove someone\n // TODO: check whether meta.username has required admin permissions\n if (meta.username !== state.settings.groupCreator) {\n throw new TypeError(L('Only the group creator can remove members.'))\n }\n } else {\n // In a big group a removal can only happen through a proposal\n const proposal = state.proposals[data.proposalHash]\n if (!proposal) {\n // TODO this\n throw new TypeError(L('Admin credentials needed and not implemented yet.'))\n }\n\n if (!proposal.payload || proposal.payload.secret !== data.proposalPayload.secret) {\n throw new TypeError(L('Invalid associated proposal.'))\n }\n }\n },\n process ({ data, meta }, { state, getters }) {\n memberLeaves(\n { username: data.member, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n },\n sideEffect ({ data, meta, contractID }, { state, getters }) {\n const rootState = sbp('state/vuex/state')\n const contracts = rootState.contracts || {}\n const { username } = rootState.loggedIn\n\n if (data.member === username) {\n // If this member is re-joining the group, ignore the rest\n // so the member doesn't remove themself again.\n if (sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n return\n }\n\n const groupIdToSwitch = Object.keys(contracts)\n .find(cID => contracts[cID].type === 'gi.contracts/group' &&\n cID !== contractID && rootState[cID].settings) || null\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {})\n sbp('state/vuex/commit', 'setCurrentGroupId', groupIdToSwitch)\n // we can't await on this in here, because it will cause a deadlock, since Chelonia processes\n // this sideEffect on the eventqueue for this contractID, and /remove uses that same eventqueue\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown by /remove ${contractID}:`, e)\n })\n // this looks crazy, but doing this was necessary to fix a race condition in the\n // group-member-removal Cypress tests where due to the ordering of asynchronous events\n // we were getting the same latestHash upon re-logging in for test \"user2 rejoins groupA\".\n // We add it to the same queue as '/remove' above gets run on so that it is run after\n // contractID is removed. See also comments in 'gi.actions/identity/login'.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/saveOurLoginState'])\n .then(function () {\n const router = sbp('controller/router')\n const switchFrom = router.currentRoute.path\n const switchTo = groupIdToSwitch ? '/dashboard' : '/'\n if (switchFrom !== '/join' && switchFrom !== switchTo) {\n router.push({ path: switchTo }).catch(console.warn)\n }\n }).catch(e => {\n console.error(`sideEffect(removeMember): ${e.name} thrown during queueEvent to ${contractID} by saveOurLoginState:`, e)\n })\n // TODO - #828 remove other group members contracts if applicable\n } else {\n const myProfile = getters.groupProfile(username)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n const memberRemovedThemselves = data.member === meta.username\n\n sbp('gi.notifications/emit', // emit a notification for a member removal.\n memberRemovedThemselves ? 'MEMBER_LEFT' : 'MEMBER_REMOVED',\n {\n groupID: contractID,\n username: memberRemovedThemselves ? meta.username : data.member\n })\n }\n // TODO - #828 remove the member contract if applicable.\n // problem is, if they're in another group we're also a part of, or if we\n // have a DM with them, we don't want to do this. may need to use manual reference counting\n // sbp('chelonia/contract/release', getters.groupProfile(data.member).contractID)\n }\n // TODO - #850 verify open proposals and see if they need some re-adjustment.\n }\n },\n 'gi.contracts/group/removeOurselves': {\n validate: objectMaybeOf({\n reason: string\n }),\n process ({ data, meta, contractID }, { state, getters }) {\n memberLeaves(\n { username: meta.username, dateLeft: meta.createdDate },\n { meta, state, getters }\n )\n\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', {\n meta,\n data: { member: meta.username, reason: data.reason || '' },\n contractID\n }]\n )\n }\n },\n 'gi.contracts/group/invite': {\n validate: inviteType,\n process ({ data, meta }, { state }) {\n Vue.set(state.invites, data.inviteSecret, data)\n }\n },\n 'gi.contracts/group/inviteAccept': {\n validate: objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n }),\n process ({ data, meta }, { state }) {\n console.debug('inviteAccept:', data, state.invites)\n const invite = state.invites[data.inviteSecret]\n if (invite.status !== INVITE_STATUS.VALID) {\n console.error(`inviteAccept: invite for ${meta.username} is: ${invite.status}`)\n return\n }\n Vue.set(invite.responses, meta.username, true)\n if (Object.keys(invite.responses).length === invite.quantity) {\n invite.status = INVITE_STATUS.USED\n }\n // TODO: ensure `meta.username` is unique for the lifetime of the username\n // since we are making it possible for the same username to leave and\n // rejoin the group. All of their past posts will be re-associated with\n // them upon re-joining.\n Vue.set(state.profiles, meta.username, initGroupProfile(meta.identityContractID, meta.createdDate))\n // If we're triggered by handleEvent in state.js (and not latestContractState)\n // then the asynchronous sideEffect function will get called next\n // and we will subscribe to this new user's identity contract\n },\n // !! IMPORANT!!\n // Actions here MUST NOT modify contract state!\n // They MUST NOT call 'commit'!\n // They should only coordinate the actions of outside contracts.\n // Otherwise `latestContractState` and `handleEvent` will not produce same state!\n async sideEffect ({ meta, contractID }, { state }) {\n const { loggedIn } = sbp('state/vuex/state')\n const { profiles = {} } = state\n\n // TODO: per #257 this will ,have to be encompassed in a recoverable transaction\n // however per #610 that might be handled in handleEvent (?), or per #356 might not be needed\n if (meta.username === loggedIn.username) {\n // we're the person who just accepted the group invite\n // so subscribe to founder's IdentityContract & everyone else's\n for (const name in profiles) {\n if (name !== loggedIn.username) {\n await sbp('chelonia/contract/sync', profiles[name].contractID)\n }\n }\n } else {\n const myProfile = profiles[loggedIn.username]\n // we're an existing member of the group getting notified that a\n // new member has joined, so subscribe to their identity contract\n await sbp('chelonia/contract/sync', meta.identityContractID)\n\n if (isActionYoungerThanUser(meta, myProfile)) {\n sbp('gi.notifications/emit', 'MEMBER_ADDED', { // emit a notification for a member addition.\n groupID: contractID,\n username: meta.username\n })\n }\n }\n }\n },\n 'gi.contracts/group/inviteRevoke': {\n validate: (data, { state, meta }) => {\n objectOf({\n inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now\n })(data)\n\n if (!state.invites[data.inviteSecret]) {\n throw new TypeError(L('The link does not exist.'))\n }\n },\n process ({ data, meta }, { state }) {\n const invite = state.invites[data.inviteSecret]\n Vue.set(invite, 'status', INVITE_STATUS.REVOKED)\n }\n },\n 'gi.contracts/group/updateSettings': {\n // OPTIMIZE: Make this custom validation function\n // reusable accross other future validators\n validate: objectMaybeOf({\n groupName: x => typeof x === 'string',\n groupPicture: x => typeof x === 'string',\n sharedValues: x => typeof x === 'string',\n mincomeAmount: x => typeof x === 'number' && x > 0,\n mincomeCurrency: x => typeof x === 'string'\n }),\n process ({ meta, data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/group/groupProfileUpdate': {\n validate: objectMaybeOf({\n incomeDetailsType: x => ['incomeAmount', 'pledgeAmount'].includes(x),\n incomeAmount: x => typeof x === 'number' && x >= 0,\n pledgeAmount: x => typeof x === 'number' && x >= 0,\n nonMonetaryAdd: string,\n nonMonetaryEdit: objectOf({\n replace: string,\n with: string\n }),\n nonMonetaryRemove: string,\n paymentMethods: arrayOf(\n objectOf({\n name: string,\n value: string\n })\n )\n }),\n process ({ data, meta }, { state, getters }) {\n const groupProfile = state.profiles[meta.username]\n const nonMonetary = groupProfile.nonMonetaryContributions\n for (const key in data) {\n const value = data[key]\n switch (key) {\n case 'nonMonetaryAdd':\n nonMonetary.push(value)\n break\n case 'nonMonetaryRemove':\n nonMonetary.splice(nonMonetary.indexOf(value), 1)\n break\n case 'nonMonetaryEdit':\n nonMonetary.splice(nonMonetary.indexOf(value.replace), 1, value.with)\n break\n default:\n Vue.set(groupProfile, key, value)\n }\n }\n if (data.incomeDetailsType) {\n // someone updated their income details, create a snapshot of the haveNeeds\n updateCurrentDistribution({ meta, state, getters })\n }\n }\n },\n 'gi.contracts/group/updateAllVotingRules': {\n validate: objectMaybeOf({\n ruleName: x => [RULE_PERCENTAGE, RULE_DISAGREEMENT].includes(x),\n ruleThreshold: number,\n expires_ms: number\n }),\n process ({ data, meta }, { state }) {\n // Update all types of proposal settings for simplicity\n if (data.ruleName && data.ruleThreshold) {\n for (const proposalSettings in state.settings.proposals) {\n Vue.set(state.settings.proposals[proposalSettings], 'rule', data.ruleName)\n Vue.set(state.settings.proposals[proposalSettings].ruleSettings[data.ruleName], 'threshold', data.ruleThreshold)\n }\n }\n\n // TODO later - support update expires_ms\n // if (data.ruleName && data.expires_ms) {\n // for (const proposalSetting in state.settings.proposals) {\n // Vue.set(state.settings.proposals[proposalSetting].ruleSettings[data.ruleName], 'expires_ms', data.expires_ms)\n // }\n // }\n }\n },\n 'gi.contracts/group/addChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n attributes: chatRoomAttributesType\n }),\n process ({ data, meta }, { state }) {\n const { name, type, privacyLevel } = data.attributes\n Vue.set(state.chatRooms, data.chatRoomID, {\n creator: meta.username,\n name,\n type,\n privacyLevel,\n deletedDate: null,\n users: []\n })\n if (!state.generalChatRoomId) {\n Vue.set(state, 'generalChatRoomId', data.chatRoomID)\n }\n }\n },\n 'gi.contracts/group/deleteChatRoom': {\n validate: (data, { getters, meta }) => {\n objectOf({ chatRoomID: string })(data)\n\n if (getters.getChatRooms[data.chatRoomID].creator !== meta.username) {\n throw new TypeError(L('Only the channel creator can delete channel.'))\n }\n },\n process ({ data, meta }, { state }) {\n Vue.delete(state.chatRooms, data.chatRoomID)\n }\n },\n 'gi.contracts/group/leaveChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n member: string,\n leavingGroup: boolean // if kicker is exists, it means group leaving\n }),\n process ({ data, meta }, { state }) {\n Vue.set(state.chatRooms[data.chatRoomID], 'users',\n state.chatRooms[data.chatRoomID].users.filter(u => u !== data.member))\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n if (meta.username === rootState.loggedIn.username && !sbp('okTurtles.data/get', 'JOINING_GROUP')) {\n const sendingData = data.leavingGroup\n ? { member: data.member }\n : { member: data.member, username: meta.username }\n await sbp('gi.actions/chatroom/leave', { contractID: data.chatRoomID, data: sendingData })\n }\n }\n },\n 'gi.contracts/group/joinChatRoom': {\n validate: objectMaybeOf({\n username: string,\n chatRoomID: string\n }),\n process ({ data, meta }, { state }) {\n const username = data.username || meta.username\n state.chatRooms[data.chatRoomID].users.push(username)\n },\n async sideEffect ({ meta, data }, { state }) {\n const rootState = sbp('state/vuex/state')\n const username = data.username || meta.username\n if (username === rootState.loggedIn.username) {\n if (!sbp('okTurtles.data/get', 'JOINING_GROUP') || sbp('okTurtles.data/get', 'READY_TO_JOIN_CHATROOM')) {\n // while users are joining chatroom, they don't need to leave chatrooms\n // this is similar to setting 'JOINING_GROUP' before joining group\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', data.chatRoomID)\n await sbp('chelonia/contract/sync', data.chatRoomID)\n sbp('okTurtles.data/set', 'JOINING_CHATROOM_ID', undefined)\n sbp('okTurtles.data/set', 'READY_TO_JOIN_CHATROOM', false)\n }\n }\n }\n },\n 'gi.contracts/group/renameChatRoom': {\n validate: objectOf({\n chatRoomID: string,\n name: string\n }),\n process ({ data, meta }, { state, getters }) {\n Vue.set(state.chatRooms, data.chatRoomID, {\n ...getters.getChatRooms[data.chatRoomID],\n name: data.name\n })\n }\n },\n ...((process.env.NODE_ENV === 'development' || process.env.CI) && {\n 'gi.contracts/group/forceDistributionDate': {\n validate: optional,\n process ({ meta }, { state, getters }) {\n getters.groupSettings.distributionDate = dateToPeriodStamp(meta.createdDate)\n }\n },\n 'gi.contracts/group/malformedMutation': {\n validate: objectOf({ errorType: string, sideEffect: optional(boolean) }),\n process ({ data }) {\n const ErrorType = Errors[data.errorType]\n if (data.sideEffect) return\n if (ErrorType) {\n throw new ErrorType('malformedMutation!')\n } else {\n throw new Error(`unknown error type: ${data.errorType}`)\n }\n },\n sideEffect (message, { state }) {\n if (!message.data.sideEffect) return\n sbp('gi.contracts/group/malformedMutation/process', {\n ...message,\n data: omit(message.data, ['sideEffect'])\n }, state)\n }\n }\n })\n // TODO: remove group profile when leave group is implemented\n },\n // methods are SBP selectors that are version-tracked for each contract.\n // in other words, you can use them to define SBP selectors that will\n // contain functions that you can modify across different contract versions,\n // and when the contract calls them, it will use that specific version of the\n // method.\n //\n // They are useful when used in conjunction with pushSideEffect from process\n // functions.\n //\n // IMPORTANT: they MUST begin with the name of the contract.\n methods: {\n 'gi.contracts/group/archiveProposal': async function (contractID, proposalHash, proposal) {\n const { username } = sbp('state/vuex/state').loggedIn\n const key = `proposals/${username}/${contractID}`\n const proposals = await sbp('gi.db/archive/load', key) || []\n // newest at the front of the array, oldest at the back\n proposals.unshift([proposalHash, proposal])\n while (proposals.length > MAX_ARCHIVED_PROPOSALS) {\n proposals.pop()\n }\n await sbp('gi.db/archive/save', key, proposals)\n sbp('okTurtles.events/emit', PROPOSAL_ARCHIVED, [proposalHash, proposal])\n }\n }\n})\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/identity.js.map b/test/contracts/identity.js.map index f0dec6c3c3..b7680a9f7b 100644 --- a/test/contracts/identity.js.map +++ b/test/contracts/identity.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/validators.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/identity.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "// Matches strings of plain ASCII letters and digits, hyphens and underscores.\nexport const allowedUsernameCharacters = (value ) => /^[\\w-]*$/.test(value)\n\n// Matches non-empty strings of plain ASCII letters and digits.\nexport const alphanumeric = (value ) => /^[A-Za-z\\d]+$/.test(value)\n\nexport const noConsecutiveHyphensOrUnderscores = (value ) => !value.includes('--') && !value.includes('__')\n\nexport const noLeadingOrTrailingHyphen = (value ) => !value.startsWith('-') && !value.endsWith('-')\nexport const noLeadingOrTrailingUnderscore = (value ) => !value.startsWith('_') && !value.endsWith('_')\n\nexport const decimals = (digits ) => (value ) => Number.isInteger(value * Math.pow(10, digits))\n\nexport const noUppercase = (value ) => value.toLowerCase() === value\n\nexport const noWhitespace = (value ) => /^\\S+$/.test(value)\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\n\nimport { Vue, L } from '@common/common.js'\nimport { merge } from './shared/giLodash.js'\nimport { objectOf, objectMaybeOf, arrayOf, string, object } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n allowedUsernameCharacters,\n noConsecutiveHyphensOrUnderscores,\n noLeadingOrTrailingHyphen,\n noLeadingOrTrailingUnderscore,\n noUppercase\n} from './shared/validators.js'\n\nimport { IDENTITY_USERNAME_MAX_CHARS } from './shared/constants.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/identity',\n getters: {\n currentIdentityState (state) {\n return state\n },\n loginState (state, getters) {\n return getters.currentIdentityState.loginState\n }\n },\n actions: {\n 'gi.contracts/identity': {\n validate: (data, { state, meta }) => {\n objectMaybeOf({\n attributes: objectMaybeOf({\n username: string,\n email: string,\n picture: string\n })\n })(data)\n const { username } = data.attributes\n if (username.length > IDENTITY_USERNAME_MAX_CHARS) {\n throw new TypeError(`A username cannot exceed ${IDENTITY_USERNAME_MAX_CHARS} characters.`)\n }\n if (!allowedUsernameCharacters(username)) {\n throw new TypeError('A username cannot contain disallowed characters.')\n }\n if (!noConsecutiveHyphensOrUnderscores(username)) {\n throw new TypeError('A username cannot contain two consecutive hyphens or underscores.')\n }\n if (!noLeadingOrTrailingHyphen(username)) {\n throw new TypeError('A username cannot start or end with a hyphen.')\n }\n if (!noLeadingOrTrailingUnderscore(username)) {\n throw new TypeError('A username cannot start or end with an underscore.')\n }\n if (!noUppercase(username)) {\n throw new TypeError('A username cannot contain uppercase letters.')\n }\n },\n process ({ data }, { state }) {\n const initialState = merge({\n settings: {},\n attributes: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/identity/setAttributes': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.attributes, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/deleteAttributes': {\n validate: arrayOf(string),\n process ({ data }, { state }) {\n for (const attribute of data) {\n Vue.delete(state.attributes, attribute)\n }\n }\n },\n 'gi.contracts/identity/updateSettings': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/setLoginState': {\n validate: objectOf({\n groupIds: arrayOf(string)\n }),\n process ({ data }, { state }) {\n Vue.set(state, 'loginState', data)\n },\n sideEffect ({ contractID }) {\n // it only makes sense to call updateLoginStateUponLogin for ourselves\n if (contractID === sbp('state/vuex/getters').ourIdentityContractId) {\n // makes sure that updateLoginStateUponLogin gets run after the entire identity\n // state has been synced, this way we don't end up joining groups we've left, etc.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/updateLoginStateUponLogin'])\n .catch((e) => {\n sbp('gi.notifications/emit', 'ERROR', {\n message: L(\"Failed to join groups we're part of on another device. Not catastrophic, but could lead to problems. {errName}: '{errMsg}'\", {\n errName: e.name,\n errMsg: e.message || '?'\n })\n })\n })\n }\n }\n }\n }\n})\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj /*: Object */, fn /*: Function */, o /*: Object */ = {}) /*: any */ {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj /*: Object */, fn /*: Function */) /*: {[any]: any} */ {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o /*: Object */, props /*: string[] */) /*: Object */ {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o /*: Object */, where /*: Function */) /*: Object */ {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array /*: Array<*> */, indices /*: Array */) /*: Array<*> */ {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o /*: Object */, props /*: string[] */) /*: {...} */ {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj /*: Object */) /*: any */ {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj /*: Object */, src /*: Object */) /*: any */ {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec /*: number */) /*: Promise */ {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length /*: number */) /*: Uint8Array */ {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length /*: number */) /*: string */ {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min /*: number */, max /*: number */) /*: number */ {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr /*: any[] */) /*: any */ {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr /*: Array<*> */) /*: Array */ {\n let flat /*: Array<*> */ = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () /*: any[] */ {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array /*: any[] */) /*: any[] */ {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a /*: any */, b /*: any */) /*: boolean */ {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func /*: Function */, wait /*: number */, immediate /*: ?boolean */) /*: Function */ {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n\n/**\n * Gets the value at `path` of `obj`. If the resolved value is\n * `undefined`, the `defaultValue` is returned in its place.\n *\n */\nexport function get (obj /*: Object */, path /*: string[] */, defaultValue /*: any */) /*: any */ {\n if (!path.length) {\n return obj\n } else if (obj === undefined) {\n return defaultValue\n }\n\n let result = obj\n let i = 0\n while (result && i < path.length) {\n result = result[path[i]]\n i++\n }\n\n return result === undefined ? defaultValue : result\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "// Matches strings of plain ASCII letters and digits, hyphens and underscores.\nexport const allowedUsernameCharacters = (value ) => /^[\\w-]*$/.test(value)\n\n// Matches non-empty strings of plain ASCII letters and digits.\nexport const alphanumeric = (value ) => /^[A-Za-z\\d]+$/.test(value)\n\nexport const noConsecutiveHyphensOrUnderscores = (value ) => !value.includes('--') && !value.includes('__')\n\nexport const noLeadingOrTrailingHyphen = (value ) => !value.startsWith('-') && !value.endsWith('-')\nexport const noLeadingOrTrailingUnderscore = (value ) => !value.startsWith('_') && !value.endsWith('_')\n\nexport const decimals = (digits ) => (value ) => Number.isInteger(value * Math.pow(10, digits))\n\nexport const noUppercase = (value ) => value.toLowerCase() === value\n\nexport const noWhitespace = (value ) => /^\\S+$/.test(value)\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\n\nimport { Vue, L } from '@common/common.js'\nimport { merge } from './shared/giLodash.js'\nimport { objectOf, objectMaybeOf, arrayOf, string, object } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n allowedUsernameCharacters,\n noConsecutiveHyphensOrUnderscores,\n noLeadingOrTrailingHyphen,\n noLeadingOrTrailingUnderscore,\n noUppercase\n} from './shared/validators.js'\n\nimport { IDENTITY_USERNAME_MAX_CHARS } from './shared/constants.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/identity',\n getters: {\n currentIdentityState (state) {\n return state\n },\n loginState (state, getters) {\n return getters.currentIdentityState.loginState\n }\n },\n actions: {\n 'gi.contracts/identity': {\n validate: (data, { state, meta }) => {\n objectMaybeOf({\n attributes: objectMaybeOf({\n username: string,\n email: string,\n picture: string\n })\n })(data)\n const { username } = data.attributes\n if (username.length > IDENTITY_USERNAME_MAX_CHARS) {\n throw new TypeError(`A username cannot exceed ${IDENTITY_USERNAME_MAX_CHARS} characters.`)\n }\n if (!allowedUsernameCharacters(username)) {\n throw new TypeError('A username cannot contain disallowed characters.')\n }\n if (!noConsecutiveHyphensOrUnderscores(username)) {\n throw new TypeError('A username cannot contain two consecutive hyphens or underscores.')\n }\n if (!noLeadingOrTrailingHyphen(username)) {\n throw new TypeError('A username cannot start or end with a hyphen.')\n }\n if (!noLeadingOrTrailingUnderscore(username)) {\n throw new TypeError('A username cannot start or end with an underscore.')\n }\n if (!noUppercase(username)) {\n throw new TypeError('A username cannot contain uppercase letters.')\n }\n },\n process ({ data }, { state }) {\n const initialState = merge({\n settings: {},\n attributes: {}\n }, data)\n for (const key in initialState) {\n Vue.set(state, key, initialState[key])\n }\n }\n },\n 'gi.contracts/identity/setAttributes': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.attributes, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/deleteAttributes': {\n validate: arrayOf(string),\n process ({ data }, { state }) {\n for (const attribute of data) {\n Vue.delete(state.attributes, attribute)\n }\n }\n },\n 'gi.contracts/identity/updateSettings': {\n validate: object,\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state.settings, key, data[key])\n }\n }\n },\n 'gi.contracts/identity/setLoginState': {\n validate: objectOf({\n groupIds: arrayOf(string)\n }),\n process ({ data }, { state }) {\n Vue.set(state, 'loginState', data)\n },\n sideEffect ({ contractID }) {\n // it only makes sense to call updateLoginStateUponLogin for ourselves\n if (contractID === sbp('state/vuex/getters').ourIdentityContractId) {\n // makes sure that updateLoginStateUponLogin gets run after the entire identity\n // state has been synced, this way we don't end up joining groups we've left, etc.\n sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/updateLoginStateUponLogin'])\n .catch((e) => {\n sbp('gi.notifications/emit', 'ERROR', {\n message: L(\"Failed to join groups we're part of on another device. Not catastrophic, but could lead to problems. {errName}: '{errMsg}'\", {\n errName: e.name,\n errMsg: e.message || '?'\n })\n })\n })\n }\n }\n }\n }\n})\n"], + "mappings": ";;;AAKA,IAAM,YAAY,CAAC;AACnB,IAAM,UAAU,CAAC;AACjB,IAAM,gBAAgB,CAAC;AACvB,IAAM,gBAAgB,CAAC;AACvB,IAAM,kBAAkB,CAAC;AACzB,IAAM,kBAAkB,CAAC;AAEzB,IAAM,eAAe;AAErB,aAAc,aAAa,MAAM;AAC/B,QAAM,SAAS,mBAAmB,QAAQ;AAC1C,MAAI,CAAC,UAAU,WAAW;AACxB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AAGA,aAAW,WAAW,CAAC,gBAAgB,WAAW,cAAc,SAAS,aAAa,GAAG;AACvF,QAAI,SAAS;AACX,iBAAW,UAAU,SAAS;AAC5B,YAAI,OAAO,QAAQ,UAAU,IAAI,MAAM;AAAO;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACA,SAAO,UAAU,UAAU,KAAK,QAAQ,QAAQ,OAAO,GAAG,IAAI;AAChE;AAEO,4BAA6B,UAAU;AAC5C,QAAM,eAAe,aAAa,KAAK,QAAQ;AAC/C,MAAI,iBAAiB,MAAM;AACzB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AACA,SAAO,aAAa;AACtB;AAEA,IAAM,qBAAqB;AAAA,EACzB,0BAA0B,SAAU,MAAM;AACxC,UAAM,aAAa,CAAC;AACpB,eAAW,YAAY,MAAM;AAC3B,YAAM,SAAS,mBAAmB,QAAQ;AAC1C,UAAI,UAAU,WAAW;AACvB,QAAC,SAAQ,QAAQ,QAAQ,KAAK,6DAA6D,WAAW;AAAA,MACxG,WAAW,OAAO,KAAK,cAAc,YAAY;AAC/C,YAAI,gBAAgB,WAAW;AAE7B,UAAC,SAAQ,QAAQ,QAAQ,KAAK,6CAA6C,gDAAgD;AAAA,QAC7H;AACA,cAAM,KAAK,UAAU,YAAY,KAAK;AACtC,mBAAW,KAAK,QAAQ;AAExB,YAAI,CAAC,QAAQ,SAAS;AACpB,kBAAQ,UAAU,EAAE,OAAO,CAAC,EAAE;AAAA,QAChC;AAEA,YAAI,aAAa,GAAG,gBAAgB;AAClC,aAAG,KAAK,QAAQ,QAAQ,KAAK;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,4BAA4B,SAAU,MAAM;AAC1C,eAAW,YAAY,MAAM;AAC3B,UAAI,CAAC,gBAAgB,WAAW;AAC9B,cAAM,IAAI,MAAM,0CAA0C,UAAU;AAAA,MACtE;AACA,aAAO,UAAU;AAAA,IACnB;AAAA,EACF;AAAA,EACA,2BAA2B,SAAU,MAAM;AACzC,QAAI,4BAA4B,OAAO,KAAK,IAAI,CAAC;AACjD,WAAO,IAAI,0BAA0B,IAAI;AAAA,EAC3C;AAAA,EACA,wBAAwB,SAAU,MAAM;AACtC,eAAW,YAAY,MAAM;AAC3B,UAAI,UAAU,WAAW;AACvB,cAAM,IAAI,MAAM,mDAAmD;AAAA,MACrE;AACA,sBAAgB,YAAY;AAAA,IAC9B;AAAA,EACF;AAAA,EACA,sBAAsB,SAAU,MAAM;AACpC,eAAW,YAAY,MAAM;AAC3B,aAAO,gBAAgB;AAAA,IACzB;AAAA,EACF;AAAA,EACA,oBAAoB,SAAU,KAAK;AACjC,WAAO,UAAU;AAAA,EACnB;AAAA,EACA,0BAA0B,SAAU,QAAQ;AAC1C,kBAAc,KAAK,MAAM;AAAA,EAC3B;AAAA,EACA,0BAA0B,SAAU,QAAQ,QAAQ;AAClD,QAAI,CAAC,cAAc;AAAS,oBAAc,UAAU,CAAC;AACrD,kBAAc,QAAQ,KAAK,MAAM;AAAA,EACnC;AAAA,EACA,4BAA4B,SAAU,UAAU,QAAQ;AACtD,QAAI,CAAC,gBAAgB;AAAW,sBAAgB,YAAY,CAAC;AAC7D,oBAAgB,UAAU,KAAK,MAAM;AAAA,EACvC;AACF;AAEA,mBAAmB,0BAA0B,kBAAkB;AAE/D,IAAO,iBAAQ;;;ACpFf;;;ACNA;AACA;;;ACwBO,mBAAoB,KAA8B;AACvD,SAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;AACvC;AAEA,2BAA4B,KAAK;AAC/B,QAAM,gBAAgB,OAAO,OAAO,QAAQ;AAE5C,SAAO,iBAAiB,OAAO,UAAU,SAAS,KAAK,GAAG,MAAM,qBAAqB,OAAO,UAAU,SAAS,KAAK,GAAG,MAAM;AAC/H;AAEO,eAAgB,KAAmB,KAA8B;AACtE,aAAW,OAAO,KAAK;AACrB,UAAM,QAAQ,kBAAkB,IAAI,IAAI,IAAI,UAAU,IAAI,IAAI,IAAI;AAClE,QAAI,SAAS,kBAAkB,IAAI,IAAI,GAAG;AACxC,YAAM,IAAI,MAAM,KAAK;AACrB;AAAA,IACF;AACA,QAAI,OAAO,SAAS,IAAI;AAAA,EAC1B;AACA,SAAO;AACT;;;ADxCO,IAAM,gBAAgB;AAAA,EAC3B,cAAc,CAAC,OAAO;AAAA,EACtB,cAAc,CAAC,KAAK,MAAM,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EAEtF,qBAAqB;AACvB;AAEA,IAAM,YAAY,CAAC,IAAI,YAAY;AACjC,MAAI,QAAQ,aAAa,QAAQ,OAAO;AACtC,QAAI,SAAS;AACb,QAAI,QAAQ,QAAQ,KAAK;AACvB,eAAS,UAAU,MAAM;AACzB,aAAO,aAAa,KAAK,QAAQ,QAAQ;AACzC,aAAO,aAAa,KAAK,GAAG;AAAA,IAC9B;AACA,OAAG,cAAc;AACjB,OAAG,YAAY,UAAU,SAAS,QAAQ,OAAO,MAAM,CAAC;AAAA,EAC1D;AACF;AAWA,IAAI,UAAU,aAAa;AAAA,EACzB,MAAM;AAAA,EACN,QAAQ;AACV,CAAC;;;AElDD;AACA;;;ACNA,IAAM,QAAQ;AAEC,kBAAmB,YAAmB,MAAqB;AACxE,QAAM,WAAW,KAAK;AAGtB,QAAM,oBACH,OAAO,aAAa,YAAY,aAAa,OAC1C,WACA;AAGN,SAAO,QAAO,QAAQ,OAAO,oBAAqB,OAAO,SAAS,OAAO;AAEvE,QAAI,QAAO,QAAQ,OAAO,OAAO,QAAO,QAAQ,MAAM,YAAY,KAAK;AACrE,aAAO;AAAA,IACT;AAEA,UAAM,mBAGJ,OAAO,UAAU,eAAe,KAAK,mBAAmB,OAAO,IAC3D,kBAAkB,WAClB;AAGN,QAAI,qBAAqB,QAAQ,qBAAqB,QAAW;AAC/D,aAAO;AAAA,IACT;AACA,WAAO,OAAO,gBAAgB;AAAA,EAChC,CAAC;AACH;;;ADtBA,KAAI,UAAU,IAAI;AAClB,KAAI,UAAU,QAAQ;AAEtB,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,0BAAgD,CAAC;AAOvD,IAAM,kBAAkB;AAAA,EACtB,GAAG;AAAA,EACH,cAAc,CAAC,SAAS,QAAQ,OAAO,QAAQ;AAAA,EAC/C,cAAc,CAAC,KAAK,KAAK,MAAM,UAAU,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EACrG,qBAAqB;AACvB;AAEA,IAAI,kBAAkB;AACtB,IAAI,sBAAsB,gBAAgB,MAAM,GAAG,EAAE;AACrD,IAAI,0BAA0B;AAY9B,eAAI,0BAA0B;AAAA,EAC5B,qBAAqB,oBAAqB,UAAiC;AAEzE,UAAM,CAAC,gBAAgB,SAAS,YAAY,EAAE,MAAM,GAAG;AAGvD,QAAI,SAAS,YAAY,MAAM,gBAAgB,YAAY;AAAG;AAI9D,QAAI,iBAAiB;AAAqB;AAG1C,QAAI,iBAAiB,qBAAqB;AACxC,wBAAkB;AAClB,4BAAsB;AACtB,gCAA0B;AAC1B;AAAA,IACF;AACA,QAAI;AACF,gCAA2B,MAAM,eAAI,4BAA4B,QAAQ,KAAM;AAG/E,wBAAkB;AAClB,4BAAsB;AAAA,IACxB,SAAS,OAAP;AACA,cAAQ,MAAM,KAAK;AAAA,IACrB;AAAA,EACF;AACF,CAAC;AA0CM,kBAAmB,MAAiC;AACzD,QAAM,IAAI;AAAA,IACR,OAAO;AAAA,EACT;AACA,aAAW,OAAO,MAAM;AACtB,MAAE,GAAG,UAAU,IAAI;AACnB,MAAE,IAAI,SAAS,KAAK;AAAA,EACtB;AACA,SAAO;AACT;AAEe,WACb,KACA,MACQ;AACR,SAAO,SAAS,wBAAwB,QAAQ,KAAK,IAAI,EAEtD,QAAQ,iBAAiB,QAAQ;AACtC;AAgBA,kBAAmB,aAAa;AAC9B,SAAO,WAAU,SAAS,aAAa,eAAe;AACxD;AAEA,KAAI,UAAU,QAAQ;AAAA,EACpB,YAAY;AAAA,EACZ,OAAO;AAAA,IACL,MAAM,CAAC,QAAQ,KAAK;AAAA,IACpB,KAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA,SAAS;AAAA,EACX;AAAA,EACA,QAAQ,SAAU,GAAG,SAAS;AAC5B,UAAM,OAAO,QAAQ,SAAS,GAAG;AACjC,UAAM,cAAc,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,CAAC;AACpD,QAAI,CAAC,aAAa;AAChB,cAAQ,KAAK,yDAAyD,IAAI;AAC1E,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,IAAI;AAAA,IAChD;AAEA,QAAI,QAAQ,MAAM,QAAQ,OAAO,QAAQ,KAAK,MAAM,WAAW,UAAU;AACvE,cAAQ,KAAK,MAAM,MAAM;AAAA,IAC3B;AACA,QAAI,QAAQ,MAAM,SAAS;AACzB,YAAM,SAAS,KAAI,QAAQ,WAAW,SAAS,WAAW,IAAI,SAAS;AAEvE,aAAO,OAAO,OAAO,KAAK;AAAA,QACxB,IAAI,CAAC,QAAQ,SAAS;AACpB,cAAI,QAAQ,QAAQ;AAClB,mBAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,GAAG,IAAI;AAAA,UACnD,OAAO;AACL,mBAAO,EAAE,KAAK,GAAG,IAAI;AAAA,UACvB;AAAA,QACF;AAAA,QACA,IAAI,OAAK;AAAA,MACX,CAAC;AAAA,IACH,OAAO;AACL,UAAI,CAAC,QAAQ,KAAK;AAAU,gBAAQ,KAAK,WAAW,CAAC;AACrD,cAAQ,KAAK,SAAS,YAAY,SAAS,WAAW;AACtD,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,IAAI;AAAA,IAC1C;AAAA,EACF;AACF,CAAC;;;AE5IM,IAAM,cAAc,OAAO,SAAS;AACpC,IAAM,UAAU,OAAK,MAAM;AAC3B,IAAM,QAAQ,OAAK,MAAM;AACzB,IAAM,UAAU,OAAK,OAAO,MAAM;AAGlC,IAAM,WAAW,OAAK,OAAO,MAAM;AACnC,IAAM,WAAW,OAAK,CAAC,MAAM,CAAC,KAAK,OAAO,MAAM;AAChD,IAAM,aAAa,OAAK,OAAO,MAAM;AAcrC,IAAM,UAAU,CAAC,QAAQ,aAAa;AAC3C,MAAI,WAAW,OAAO,IAAI;AAAG,WAAO,OAAO,KAAK,QAAQ;AACxD,SAAO,OAAO,QAAQ;AACxB;AAGO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YACE,SACA,cACA,WACA,OACA,WAAmB,IACnB,YAAqB,IACrB;AACA,UAAM,aAAa,WACjB,YAAY,0BAA0B,YAAY;AACpD,UAAM,UAAU;AAChB,SAAK,eAAe;AACpB,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,YAAY,aAAa;AAC9B,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,UAAU,GAAG;AAAA,EAAe,KAAK,aAAa;AACnD,SAAK,OAAO,KAAK,YAAY;AAC7B,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,kBAAkB;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,gBAAyB;AACvB,UAAM,YAAY,KAAK,MAAM,MAAM,gCAAgC,KAAK,CAAC;AACzE,WAAO,UAAU,KAAK,cAAY,SAAS,QAAQ,qBAAqB,MAAM,EAAE,KAAK;AAAA,EACvF;AAAA,EAEA,eAAwB;AACtB,WAAO;AAAA,eACI,KAAK;AAAA,eACL,KAAK;AAAA,eACL,KAAK,aAAa,QAAQ,OAAO,EAAE;AAAA,eACnC,KAAK;AAAA,eACL,KAAK;AAAA;AAAA,EAElB;AACF;AAKA,IAAM,iBAAoB,CACxB,QACA,OACA,OACA,SACA,cACA,cACuB;AACvB,SAAO,IAAI,mBACT,SACA,gBAAgB,QAAQ,MAAM,GAC9B,aAAa,OAAO,OACpB,KAAK,UAAU,KAAK,GACpB,OAAO,MACP,KACF;AACF;AAEO,IAAM,UACR,CAAC,QAA0B,SAAkB,YAAmC;AACjF,iBAAgB,OAAO;AACrB,QAAI,QAAQ,KAAK;AAAG,aAAO,CAAC,OAAO,KAAK,CAAC;AACzC,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,UAAI,QAAQ;AACZ,aAAO,MAAM,IAAI,OAAK,OAAO,GAAG,GAAG,UAAU,UAAU,CAAC;AAAA,IAC1D;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,SAAS,QAAQ,MAAM;AAC1C,SAAO;AACT;AAsDK,IAAM,SACX,SAAU,OAAO;AACf,MAAI,QAAQ,KAAK;AAAG,WAAO,CAAC;AAC5B,MAAI,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,KAAK,GAAG;AAC5C,WAAO,OAAO,OAAO,CAAC,GAAG,KAAK;AAAA,EAChC;AACA,QAAM,eAAe,QAAQ,KAAK;AACpC;AAGK,IAAM,WACX,CAAC,SAAY,SAAkB,aAAoE;AACnG,mBAAkB,OAAO;AACvB,UAAM,IAAI,OAAO,KAAK;AACtB,UAAM,YAAY,OAAO,KAAK,OAAO;AACrC,UAAM,cAAc,OAAO,KAAK,CAAC,EAAE,KAAK,UAAQ,CAAC,UAAU,SAAS,IAAI,CAAC;AACzE,QAAI,aAAa;AACf,YAAM,eACJ,SACA,OACA,QACA,4BAA4B,mBAAmB,aACjD;AAAA,IACF;AAIA,UAAM,YAAY,UAAU,KAAK,cAAY;AAC3C,YAAM,iBAAiB,QAAQ;AAC/B,aAAQ,eAAe,KAAK,SAAS,OAAO,KAAK,CAAC,EAAE,eAAe,QAAQ;AAAA,IAC7E,CAAC;AACD,QAAI,WAAW;AACb,YAAM,eACJ,SACA,EAAE,YACF,GAAG,UAAU,aACb,0BAA0B,kBAAkB,eAC5C,iBAAiB,QAAQ,QAAQ,UAAU,EAAE,OAAO,CAAC,KACrD,GACF;AAAA,IACF;AAEA,UAAM,UAAU,QAAQ,KAAK,IACzB,CAAC,KAAK,QAAQ,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,CAAC,IAC/D,CAAC,KAAK,QAAQ;AACd,YAAM,SAAS,QAAQ;AACvB,UAAI,OAAO,KAAK,SAAS,UAAU,KAAK,CAAC,EAAE,eAAe,GAAG,GAAG;AAC9D,eAAO,OAAO,OAAO,KAAK,CAAC,CAAC;AAAA,MAC9B,OAAO;AACL,eAAO,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,OAAO,EAAE,MAAM,GAAG,UAAU,KAAK,EAAE,CAAC;AAAA,MACzE;AAAA,IACF;AACF,WAAO,UAAU,OAAO,SAAS,CAAC,CAAC;AAAA,EACrC;AACA,UAAQ,OAAO,MAAM;AACnB,UAAM,QAAQ,OAAO,KAAK,OAAO,EAAE,IACjC,CAAC,QAAQ;AACP,YAAM,MAAM,QAAQ,KAAK,KAAK,SAAS,UAAU,IAC/C,GAAG,SAAS,QAAQ,QAAQ,MAAM,EAAE,QAAQ,KAAK,CAAC,MAClD,GAAG,QAAQ,QAAQ,QAAQ,IAAI;AACjC,aAAO;AAAA,IACT,CACF;AACA,WAAO;AAAA,GAAQ,MAAM,KAAK,OAAO;AAAA;AAAA,EACnC;AACA,SAAO;AACT;AAGO,uBAAwB,aAAqB,SAAkB,UAAkB;AACtF,SAAO,SAAU,MAAW;AAC1B,WAAO,IAAI;AACX,eAAW,OAAO,MAAM;AACtB,kBAAY,OAAO,KAAK,MAAM,GAAG,UAAU,KAAK;AAAA,IAClD;AACA,WAAO;AAAA,EACT;AACF;AAoBO,eAAgB,OAAO,SAAS,IAAI;AACzC,MAAI,QAAQ,KAAK,KAAK,QAAQ,KAAK;AAAG,WAAO;AAC7C,QAAM,eAAe,OAAO,OAAO,MAAM;AAC3C;AACA,MAAM,OAAO,MAAM;AAqBZ,IAAM,SACX,iBAAiB,OAAO,SAAS,IAAI;AACnC,MAAI,QAAQ,KAAK;AAAG,WAAO;AAC3B,MAAI,SAAS,KAAK;AAAG,WAAO;AAC5B,QAAM,eAAe,SAAQ,OAAO,MAAM;AAC5C;;;AClVK,IAAM,4BAA4B,CAAC,UAA2B,WAAW,KAAK,KAAK;AAKnF,IAAM,oCAAoC,CAAC,UAA2B,CAAC,MAAM,SAAS,IAAI,KAAK,CAAC,MAAM,SAAS,IAAI;AAEnH,IAAM,4BAA4B,CAAC,UAA2B,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,SAAS,GAAG;AAC3G,IAAM,gCAAgC,CAAC,UAA2B,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,SAAS,GAAG;AAI/G,IAAM,cAAc,CAAC,UAA2B,MAAM,YAAY,MAAM;;;ACRxE,IAAM,8BAA8B;;;ACY3C,eAAI,2BAA2B;AAAA,EAC7B,MAAM;AAAA,EACN,SAAS;AAAA,IACP,qBAAsB,OAAO;AAC3B,aAAO;AAAA,IACT;AAAA,IACA,WAAY,OAAO,SAAS;AAC1B,aAAO,QAAQ,qBAAqB;AAAA,IACtC;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,yBAAyB;AAAA,MACvB,UAAU,CAAC,MAAM,EAAE,OAAO,WAAW;AACnC,sBAAc;AAAA,UACZ,YAAY,cAAc;AAAA,YACxB,UAAU;AAAA,YACV,OAAO;AAAA,YACP,SAAS;AAAA,UACX,CAAC;AAAA,QACH,CAAC,EAAE,IAAI;AACP,cAAM,EAAE,aAAa,KAAK;AAC1B,YAAI,SAAS,SAAS,6BAA6B;AACjD,gBAAM,IAAI,UAAU,4BAA4B,yCAAyC;AAAA,QAC3F;AACA,YAAI,CAAC,0BAA0B,QAAQ,GAAG;AACxC,gBAAM,IAAI,UAAU,kDAAkD;AAAA,QACxE;AACA,YAAI,CAAC,kCAAkC,QAAQ,GAAG;AAChD,gBAAM,IAAI,UAAU,mEAAmE;AAAA,QACzF;AACA,YAAI,CAAC,0BAA0B,QAAQ,GAAG;AACxC,gBAAM,IAAI,UAAU,+CAA+C;AAAA,QACrE;AACA,YAAI,CAAC,8BAA8B,QAAQ,GAAG;AAC5C,gBAAM,IAAI,UAAU,oDAAoD;AAAA,QAC1E;AACA,YAAI,CAAC,YAAY,QAAQ,GAAG;AAC1B,gBAAM,IAAI,UAAU,8CAA8C;AAAA,QACpE;AAAA,MACF;AAAA,MACA,QAAS,EAAE,QAAQ,EAAE,SAAS;AAC5B,cAAM,eAAe,MAAM;AAAA,UACzB,UAAU,CAAC;AAAA,UACX,YAAY,CAAC;AAAA,QACf,GAAG,IAAI;AACP,mBAAW,OAAO,cAAc;AAC9B,mBAAI,IAAI,OAAO,KAAK,aAAa,IAAI;AAAA,QACvC;AAAA,MACF;AAAA,IACF;AAAA,IACA,uCAAuC;AAAA,MACrC,UAAU;AAAA,MACV,QAAS,EAAE,QAAQ,EAAE,SAAS;AAC5B,mBAAW,OAAO,MAAM;AACtB,mBAAI,IAAI,MAAM,YAAY,KAAK,KAAK,IAAI;AAAA,QAC1C;AAAA,MACF;AAAA,IACF;AAAA,IACA,0CAA0C;AAAA,MACxC,UAAU,QAAQ,MAAM;AAAA,MACxB,QAAS,EAAE,QAAQ,EAAE,SAAS;AAC5B,mBAAW,aAAa,MAAM;AAC5B,mBAAI,OAAO,MAAM,YAAY,SAAS;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,IACA,wCAAwC;AAAA,MACtC,UAAU;AAAA,MACV,QAAS,EAAE,QAAQ,EAAE,SAAS;AAC5B,mBAAW,OAAO,MAAM;AACtB,mBAAI,IAAI,MAAM,UAAU,KAAK,KAAK,IAAI;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,IACA,uCAAuC;AAAA,MACrC,UAAU,SAAS;AAAA,QACjB,UAAU,QAAQ,MAAM;AAAA,MAC1B,CAAC;AAAA,MACD,QAAS,EAAE,QAAQ,EAAE,SAAS;AAC5B,iBAAI,IAAI,OAAO,cAAc,IAAI;AAAA,MACnC;AAAA,MACA,WAAY,EAAE,cAAc;AAE1B,YAAI,eAAe,eAAI,oBAAoB,EAAE,uBAAuB;AAGlE,yBAAI,4BAA4B,YAAY,CAAC,+CAA+C,CAAC,EAC1F,MAAM,CAAC,MAAM;AACZ,2BAAI,yBAAyB,SAAS;AAAA,cACpC,SAAS,EAAE,8HAA8H;AAAA,gBACvI,SAAS,EAAE;AAAA,gBACX,QAAQ,EAAE,WAAW;AAAA,cACvB,CAAC;AAAA,YACH,CAAC;AAAA,UACH,CAAC;AAAA,QACL;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;", "names": [] } diff --git a/test/contracts/mailbox.js.map b/test/contracts/mailbox.js.map index 25423b0e63..bd65566c0a 100644 --- a/test/contracts/mailbox.js.map +++ b/test/contracts/mailbox.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../node_modules/@sbp/sbp/dist/module.mjs", "../../frontend/common/common.js", "../../frontend/common/vSafeHtml.js", "../../frontend/model/contracts/shared/giLodash.js", "../../frontend/common/translations.js", "../../frontend/common/stringTemplate.js", "../../frontend/model/contracts/misc/flowTyper.js", "../../frontend/model/contracts/shared/constants.js", "../../frontend/model/contracts/shared/types.js", "../../frontend/model/contracts/mailbox.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { mailType } from './shared/types.js'\nimport { objectOf, string, object, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/mailbox',\n metadata: {\n // TODO: why is this missing the from username..?\n validate: objectOf({\n createdDate: string\n }),\n create () {\n return {\n createdDate: new Date().toISOString()\n }\n }\n },\n actions: {\n 'gi.contracts/mailbox': {\n validate: object, // TODO: define this\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state, key, data[key])\n }\n Vue.set(state, 'messages', [])\n }\n },\n 'gi.contracts/mailbox/postMessage': {\n validate: objectOf({\n messageType: mailType,\n from: string,\n subject: optional(string),\n message: optional(string),\n headers: optional(object)\n }),\n process (message, { state }) {\n state.messages.push(message)\n }\n },\n 'gi.contracts/mailbox/authorizeSender': {\n validate: objectOf({\n sender: string\n }),\n process ({ data }, { state }) {\n // TODO: replace this via OP_KEY_*?\n throw new Error('unimplemented!')\n }\n }\n }\n})\n"], - "mappings": ";;;AAKA,IAAM,YAAY,CAAC;AACnB,IAAM,UAAU,CAAC;AACjB,IAAM,gBAAgB,CAAC;AACvB,IAAM,gBAAgB,CAAC;AACvB,IAAM,kBAAkB,CAAC;AACzB,IAAM,kBAAkB,CAAC;AAEzB,IAAM,eAAe;AAErB,aAAc,aAAa,MAAM;AAC/B,QAAM,SAAS,mBAAmB,QAAQ;AAC1C,MAAI,CAAC,UAAU,WAAW;AACxB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AAGA,aAAW,WAAW,CAAC,gBAAgB,WAAW,cAAc,SAAS,aAAa,GAAG;AACvF,QAAI,SAAS;AACX,iBAAW,UAAU,SAAS;AAC5B,YAAI,OAAO,QAAQ,UAAU,IAAI,MAAM;AAAO;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACA,SAAO,UAAU,UAAU,KAAK,QAAQ,QAAQ,OAAO,GAAG,IAAI;AAChE;AAEO,4BAA6B,UAAU;AAC5C,QAAM,eAAe,aAAa,KAAK,QAAQ;AAC/C,MAAI,iBAAiB,MAAM;AACzB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AACA,SAAO,aAAa;AACtB;AAEA,IAAM,qBAAqB;AAAA,EACzB,0BAA0B,SAAU,MAAM;AACxC,UAAM,aAAa,CAAC;AACpB,eAAW,YAAY,MAAM;AAC3B,YAAM,SAAS,mBAAmB,QAAQ;AAC1C,UAAI,UAAU,WAAW;AACvB,QAAC,SAAQ,QAAQ,QAAQ,KAAK,6DAA6D,WAAW;AAAA,MACxG,WAAW,OAAO,KAAK,cAAc,YAAY;AAC/C,YAAI,gBAAgB,WAAW;AAE7B,UAAC,SAAQ,QAAQ,QAAQ,KAAK,6CAA6C,gDAAgD;AAAA,QAC7H;AACA,cAAM,KAAK,UAAU,YAAY,KAAK;AACtC,mBAAW,KAAK,QAAQ;AAExB,YAAI,CAAC,QAAQ,SAAS;AACpB,kBAAQ,UAAU,EAAE,OAAO,CAAC,EAAE;AAAA,QAChC;AAEA,YAAI,aAAa,GAAG,gBAAgB;AAClC,aAAG,KAAK,QAAQ,QAAQ,KAAK;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,4BAA4B,SAAU,MAAM;AAC1C,eAAW,YAAY,MAAM;AAC3B,UAAI,CAAC,gBAAgB,WAAW;AAC9B,cAAM,IAAI,MAAM,0CAA0C,UAAU;AAAA,MACtE;AACA,aAAO,UAAU;AAAA,IACnB;AAAA,EACF;AAAA,EACA,2BAA2B,SAAU,MAAM;AACzC,QAAI,4BAA4B,OAAO,KAAK,IAAI,CAAC;AACjD,WAAO,IAAI,0BAA0B,IAAI;AAAA,EAC3C;AAAA,EACA,wBAAwB,SAAU,MAAM;AACtC,eAAW,YAAY,MAAM;AAC3B,UAAI,UAAU,WAAW;AACvB,cAAM,IAAI,MAAM,mDAAmD;AAAA,MACrE;AACA,sBAAgB,YAAY;AAAA,IAC9B;AAAA,EACF;AAAA,EACA,sBAAsB,SAAU,MAAM;AACpC,eAAW,YAAY,MAAM;AAC3B,aAAO,gBAAgB;AAAA,IACzB;AAAA,EACF;AAAA,EACA,oBAAoB,SAAU,KAAK;AACjC,WAAO,UAAU;AAAA,EACnB;AAAA,EACA,0BAA0B,SAAU,QAAQ;AAC1C,kBAAc,KAAK,MAAM;AAAA,EAC3B;AAAA,EACA,0BAA0B,SAAU,QAAQ,QAAQ;AAClD,QAAI,CAAC,cAAc;AAAS,oBAAc,UAAU,CAAC;AACrD,kBAAc,QAAQ,KAAK,MAAM;AAAA,EACnC;AAAA,EACA,4BAA4B,SAAU,UAAU,QAAQ;AACtD,QAAI,CAAC,gBAAgB;AAAW,sBAAgB,YAAY,CAAC;AAC7D,oBAAgB,UAAU,KAAK,MAAM;AAAA,EACvC;AACF;AAEA,mBAAmB,0BAA0B,kBAAkB;AAE/D,IAAO,iBAAQ;;;ACpFf;;;ACNA;AACA;;;ACwBO,mBAAoB,KAAK;AAC9B,SAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;AACvC;;;ADtBO,IAAM,gBAAgB;AAAA,EAC3B,cAAc,CAAC,OAAO;AAAA,EACtB,cAAc,CAAC,KAAK,MAAM,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EAEtF,qBAAqB;AACvB;AAEA,IAAM,YAAY,CAAC,IAAI,YAAY;AACjC,MAAI,QAAQ,aAAa,QAAQ,OAAO;AACtC,QAAI,SAAS;AACb,QAAI,QAAQ,QAAQ,KAAK;AACvB,eAAS,UAAU,MAAM;AACzB,aAAO,aAAa,KAAK,QAAQ,QAAQ;AACzC,aAAO,aAAa,KAAK,GAAG;AAAA,IAC9B;AACA,OAAG,cAAc;AACjB,OAAG,YAAY,UAAU,SAAS,QAAQ,OAAO,MAAM,CAAC;AAAA,EAC1D;AACF;AAWA,IAAI,UAAU,aAAa;AAAA,EACzB,MAAM;AAAA,EACN,QAAQ;AACV,CAAC;;;AElDD;AACA;;;ACNA,IAAM,QAAQ;AAEC,kBAAmB,YAAmB,MAAqB;AACxE,QAAM,WAAW,KAAK;AAGtB,QAAM,oBACH,OAAO,aAAa,YAAY,aAAa,OAC1C,WACA;AAGN,SAAO,QAAO,QAAQ,OAAO,oBAAqB,OAAO,SAAS,OAAO;AAEvE,QAAI,QAAO,QAAQ,OAAO,OAAO,QAAO,QAAQ,MAAM,YAAY,KAAK;AACrE,aAAO;AAAA,IACT;AAEA,UAAM,mBAGJ,OAAO,UAAU,eAAe,KAAK,mBAAmB,OAAO,IAC3D,kBAAkB,WAClB;AAGN,QAAI,qBAAqB,QAAQ,qBAAqB,QAAW;AAC/D,aAAO;AAAA,IACT;AACA,WAAO,OAAO,gBAAgB;AAAA,EAChC,CAAC;AACH;;;ADtBA,KAAI,UAAU,IAAI;AAClB,KAAI,UAAU,QAAQ;AAEtB,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,0BAAgD,CAAC;AAOvD,IAAM,kBAAkB;AAAA,EACtB,GAAG;AAAA,EACH,cAAc,CAAC,SAAS,QAAQ,OAAO,QAAQ;AAAA,EAC/C,cAAc,CAAC,KAAK,KAAK,MAAM,UAAU,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EACrG,qBAAqB;AACvB;AAEA,IAAI,kBAAkB;AACtB,IAAI,sBAAsB,gBAAgB,MAAM,GAAG,EAAE;AACrD,IAAI,0BAA0B;AAY9B,eAAI,0BAA0B;AAAA,EAC5B,qBAAqB,oBAAqB,UAAiC;AAEzE,UAAM,CAAC,gBAAgB,SAAS,YAAY,EAAE,MAAM,GAAG;AAGvD,QAAI,SAAS,YAAY,MAAM,gBAAgB,YAAY;AAAG;AAI9D,QAAI,iBAAiB;AAAqB;AAG1C,QAAI,iBAAiB,qBAAqB;AACxC,wBAAkB;AAClB,4BAAsB;AACtB,gCAA0B;AAC1B;AAAA,IACF;AACA,QAAI;AACF,gCAA2B,MAAM,eAAI,4BAA4B,QAAQ,KAAM;AAG/E,wBAAkB;AAClB,4BAAsB;AAAA,IACxB,SAAS,OAAP;AACA,cAAQ,MAAM,KAAK;AAAA,IACrB;AAAA,EACF;AACF,CAAC;AA0CM,kBAAmB,MAAiC;AACzD,QAAM,IAAI;AAAA,IACR,OAAO;AAAA,EACT;AACA,aAAW,OAAO,MAAM;AACtB,MAAE,GAAG,UAAU,IAAI;AACnB,MAAE,IAAI,SAAS,KAAK;AAAA,EACtB;AACA,SAAO;AACT;AAEe,WACb,KACA,MACQ;AACR,SAAO,SAAS,wBAAwB,QAAQ,KAAK,IAAI,EAEtD,QAAQ,iBAAiB,QAAQ;AACtC;AAgBA,kBAAmB,aAAa;AAC9B,SAAO,WAAU,SAAS,aAAa,eAAe;AACxD;AAEA,KAAI,UAAU,QAAQ;AAAA,EACpB,YAAY;AAAA,EACZ,OAAO;AAAA,IACL,MAAM,CAAC,QAAQ,KAAK;AAAA,IACpB,KAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA,SAAS;AAAA,EACX;AAAA,EACA,QAAQ,SAAU,GAAG,SAAS;AAC5B,UAAM,OAAO,QAAQ,SAAS,GAAG;AACjC,UAAM,cAAc,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,CAAC;AACpD,QAAI,CAAC,aAAa;AAChB,cAAQ,KAAK,yDAAyD,IAAI;AAC1E,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,IAAI;AAAA,IAChD;AAEA,QAAI,QAAQ,MAAM,QAAQ,OAAO,QAAQ,KAAK,MAAM,WAAW,UAAU;AACvE,cAAQ,KAAK,MAAM,MAAM;AAAA,IAC3B;AACA,QAAI,QAAQ,MAAM,SAAS;AACzB,YAAM,SAAS,KAAI,QAAQ,WAAW,SAAS,WAAW,IAAI,SAAS;AAEvE,aAAO,OAAO,OAAO,KAAK;AAAA,QACxB,IAAI,CAAC,QAAQ,SAAS;AACpB,cAAI,QAAQ,QAAQ;AAClB,mBAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,GAAG,IAAI;AAAA,UACnD,OAAO;AACL,mBAAO,EAAE,KAAK,GAAG,IAAI;AAAA,UACvB;AAAA,QACF;AAAA,QACA,IAAI,OAAK;AAAA,MACX,CAAC;AAAA,IACH,OAAO;AACL,UAAI,CAAC,QAAQ,KAAK;AAAU,gBAAQ,KAAK,WAAW,CAAC;AACrD,cAAQ,KAAK,SAAS,YAAY,SAAS,WAAW;AACtD,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,IAAI;AAAA,IAC1C;AAAA,EACF;AACF,CAAC;;;AE5IM,IAAM,cAAc,OAAO,SAAS;AACpC,IAAM,UAAU,OAAK,MAAM;AAC3B,IAAM,QAAQ,OAAK,MAAM;AACzB,IAAM,UAAU,OAAK,OAAO,MAAM;AAClC,IAAM,YAAY,OAAK,OAAO,MAAM;AACpC,IAAM,WAAW,OAAK,OAAO,MAAM;AACnC,IAAM,WAAW,OAAK,OAAO,MAAM;AACnC,IAAM,WAAW,OAAK,CAAC,MAAM,CAAC,KAAK,OAAO,MAAM;AAChD,IAAM,aAAa,OAAK,OAAO,MAAM;AAcrC,IAAM,UAAU,CAAC,QAAQ,aAAa;AAC3C,MAAI,WAAW,OAAO,IAAI;AAAG,WAAO,OAAO,KAAK,QAAQ;AACxD,SAAO,OAAO,QAAQ;AACxB;AAGO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YACE,SACA,cACA,WACA,OACA,WAAmB,IACnB,YAAqB,IACrB;AACA,UAAM,aAAa,WACjB,YAAY,0BAA0B,YAAY;AACpD,UAAM,UAAU;AAChB,SAAK,eAAe;AACpB,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,YAAY,aAAa;AAC9B,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,UAAU,GAAG;AAAA,EAAe,KAAK,aAAa;AACnD,SAAK,OAAO,KAAK,YAAY;AAC7B,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,kBAAkB;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,gBAAyB;AACvB,UAAM,YAAY,KAAK,MAAM,MAAM,gCAAgC,KAAK,CAAC;AACzE,WAAO,UAAU,KAAK,cAAY,SAAS,QAAQ,qBAAqB,MAAM,EAAE,KAAK;AAAA,EACvF;AAAA,EAEA,eAAwB;AACtB,WAAO;AAAA,eACI,KAAK;AAAA,eACL,KAAK;AAAA,eACL,KAAK,aAAa,QAAQ,OAAO,EAAE;AAAA,eACnC,KAAK;AAAA,eACL,KAAK;AAAA;AAAA,EAElB;AACF;AAKA,IAAM,iBAAoB,CACxB,QACA,OACA,OACA,SACA,cACA,cACuB;AACvB,SAAO,IAAI,mBACT,SACA,gBAAgB,QAAQ,MAAM,GAC9B,aAAa,OAAO,OACpB,KAAK,UAAU,KAAK,GACpB,OAAO,MACP,KACF;AACF;AAEO,IAAM,UACR,CAAC,QAA0B,SAAkB,YAAmC;AACjF,iBAAgB,OAAO;AACrB,QAAI,QAAQ,KAAK;AAAG,aAAO,CAAC,OAAO,KAAK,CAAC;AACzC,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,UAAI,QAAQ;AACZ,aAAO,MAAM,IAAI,OAAK,OAAO,GAAG,GAAG,UAAU,UAAU,CAAC;AAAA,IAC1D;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,SAAS,QAAQ,MAAM;AAC1C,SAAO;AACT;AAEK,IAAM,YACM,CAAC,cAAmC;AACnD,mBAAkB,OAAO,SAAS,IAAI;AACpC,QAAI,QAAQ,KAAK,KAAM,UAAU;AAAY,aAAO;AACpD,UAAM,eAAe,SAAS,OAAO,MAAM;AAAA,EAC7C;AACA,UAAQ,OAAO,MAAM;AACnB,QAAI,UAAU,SAAS;AAAG,aAAO,GAAG,YAAY,SAAS;AAAA;AACpD,aAAO,IAAI;AAAA,EAClB;AACA,SAAO;AACT;AAEK,IAAM,QAAc,CACzB,WACA,WAC8B;AAC9B,kBAAgB,OAAO;AACrB,QAAI,QAAQ,KAAK;AAAG,aAAO,CAAC;AAC5B,UAAM,IAAI,OAAO,KAAK;AACtB,UAAM,UAAU,CAAC,KAAK,QACpB,OAAO,OACL,KACA;AAAA,MAEE,CAAC,UAAU,KAAK,QAAQ,IAAI,OAAO,EAAE,MAAM,OAAO,KAAK;AAAA,IACzD,CACF;AACF,WAAO,OAAO,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC,CAAC;AAAA,EAC1C;AACA,SAAM,OAAO,MAAM,QAAQ,QAAQ,SAAS,OAAO,QAAQ,MAAM;AACjE,SAAO;AACT;AAoBO,IAAM,SACX,SAAU,OAAO;AACf,MAAI,QAAQ,KAAK;AAAG,WAAO,CAAC;AAC5B,MAAI,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,KAAK,GAAG;AAC5C,WAAO,OAAO,OAAO,CAAC,GAAG,KAAK;AAAA,EAChC;AACA,QAAM,eAAe,QAAQ,KAAK;AACpC;AAGK,IAAM,WACX,CAAC,SAAY,SAAkB,aAAoE;AACnG,mBAAkB,OAAO;AACvB,UAAM,IAAI,OAAO,KAAK;AACtB,UAAM,YAAY,OAAO,KAAK,OAAO;AACrC,UAAM,cAAc,OAAO,KAAK,CAAC,EAAE,KAAK,UAAQ,CAAC,UAAU,SAAS,IAAI,CAAC;AACzE,QAAI,aAAa;AACf,YAAM,eACJ,SACA,OACA,QACA,4BAA4B,mBAAmB,aACjD;AAAA,IACF;AAIA,UAAM,YAAY,UAAU,KAAK,cAAY;AAC3C,YAAM,iBAAiB,QAAQ;AAC/B,aAAQ,eAAe,KAAK,SAAS,OAAO,KAAK,CAAC,EAAE,eAAe,QAAQ;AAAA,IAC7E,CAAC;AACD,QAAI,WAAW;AACb,YAAM,eACJ,SACA,EAAE,YACF,GAAG,UAAU,aACb,0BAA0B,kBAAkB,eAC5C,iBAAiB,QAAQ,QAAQ,UAAU,EAAE,OAAO,CAAC,KACrD,GACF;AAAA,IACF;AAEA,UAAM,UAAU,QAAQ,KAAK,IACzB,CAAC,KAAK,QAAQ,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,CAAC,IAC/D,CAAC,KAAK,QAAQ;AACd,YAAM,SAAS,QAAQ;AACvB,UAAI,OAAO,KAAK,SAAS,UAAU,KAAK,CAAC,EAAE,eAAe,GAAG,GAAG;AAC9D,eAAO,OAAO,OAAO,KAAK,CAAC,CAAC;AAAA,MAC9B,OAAO;AACL,eAAO,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,OAAO,EAAE,MAAM,GAAG,UAAU,KAAK,EAAE,CAAC;AAAA,MACzE;AAAA,IACF;AACF,WAAO,UAAU,OAAO,SAAS,CAAC,CAAC;AAAA,EACrC;AACA,UAAQ,OAAO,MAAM;AACnB,UAAM,QAAQ,OAAO,KAAK,OAAO,EAAE,IACjC,CAAC,QAAQ;AACP,YAAM,MAAM,QAAQ,KAAK,KAAK,SAAS,UAAU,IAC/C,GAAG,SAAS,QAAQ,QAAQ,MAAM,EAAE,QAAQ,KAAK,CAAC,MAClD,GAAG,QAAQ,QAAQ,QAAQ,IAAI;AACjC,aAAO;AAAA,IACT,CACF;AACA,WAAO;AAAA,GAAQ,MAAM,KAAK,OAAO;AAAA;AAAA,EACnC;AACA,SAAO;AACT;AAGO,uBAAwB,aAAqB,SAAkB,UAAkB;AACtF,SAAO,SAAU,MAAW;AAC1B,WAAO,IAAI;AACX,eAAW,OAAO,MAAM;AACtB,kBAAY,OAAO,KAAK,MAAM,GAAG,UAAU,KAAK;AAAA,IAClD;AACA,WAAO;AAAA,EACT;AACF;AAEO,IAAM,WACR,CAAC,WAAsD;AACxD,QAAM,UAAU,QAAQ,QAAQ,KAAK;AACrC,qBAAmB,GAAG;AACpB,WAAO,QAAQ,CAAC;AAAA,EAClB;AACA,YAAS,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,QAAQ,OAAO,IAAI,QAAQ,MAAM;AAC3E,SAAO;AACT;AAUK,eAAgB,OAAO,SAAS,IAAI;AACzC,MAAI,QAAQ,KAAK,KAAK,QAAQ,KAAK;AAAG,WAAO;AAC7C,QAAM,eAAe,OAAO,OAAO,MAAM;AAC3C;AACA,MAAM,OAAO,MAAM;AAYZ,IAAM,SACX,iBAAiB,OAAO,SAAS,IAAI;AACnC,MAAI,QAAQ,KAAK;AAAG,WAAO;AAC3B,MAAI,SAAS,KAAK;AAAG,WAAO;AAC5B,QAAM,eAAe,SAAQ,OAAO,MAAM;AAC5C;AAIK,IAAM,SACX,iBAAiB,OAAO,SAAS,IAAI;AACnC,MAAI,QAAQ,KAAK;AAAG,WAAO;AAC3B,MAAI,SAAS,KAAK;AAAG,WAAO;AAC5B,QAAM,eAAe,SAAQ,OAAO,MAAM;AAC5C;AAkDF,qBAAsB,WAAW;AAC/B,iBAAgB,OAAc,SAAS,IAAI;AACzC,eAAW,UAAU,WAAW;AAC9B,UAAI;AACF,eAAO,OAAO,OAAO,MAAM;AAAA,MAC7B,SAAS,GAAP;AAAA,MAAW;AAAA,IACf;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,IAAI,UAAU,IAAI,QAAM,QAAQ,EAAE,CAAC,EAAE,KAAK,KAAK;AAClE,SAAO;AACT;AAGO,IAAM,UAAU;;;AChWhB,IAAM,iBAAiB;AAAA,EAC5B,YAAY;AAAA,EACZ,OAAO;AACT;AAEO,IAAM,yBAAyB;AAAA,EACpC,OAAO;AAAA,EACP,SAAS;AAAA,EACT,QAAQ;AACV;AAEO,IAAM,gBAAgB;AAAA,EAC3B,MAAM;AAAA,EACN,MAAM;AAAA,EACN,aAAa;AAAA,EACb,cAAc;AAChB;AAOO,IAAM,wBAAwB;AAAA,EACnC,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,MAAM;AACR;AAWO,IAAM,oBAAoB;AAC1B,IAAM,uBAAuB;;;ACjF7B,IAAM,aAAkB,SAAS;AAAA,EACtC,cAAc;AAAA,EACd,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS,SAAS,MAAM;AAAA,EACxB,QAAQ;AAAA,EACR,WAAW,MAAM,QAAQ,MAAM;AAAA,EAC/B,SAAS;AACX,CAAC;AAIM,IAAM,yBAA8B,SAAS;AAAA,EAClD,MAAM;AAAA,EACN,aAAa;AAAA,EACb,MAAM,QAAQ,GAAG,OAAO,OAAO,cAAc,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAAA,EACrE,cAAc,QAAQ,GAAG,OAAO,OAAO,sBAAsB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AACvF,CAAC;AAEM,IAAM,cAAmB,cAAc;AAAA,EAC5C,MAAM,QAAQ,GAAG,OAAO,OAAO,aAAa,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAAA,EACpE,MAAM;AAAA,EACN,cAAc,cAAc;AAAA,IAC1B,MAAM,QAAQ,GAAG,OAAO,OAAO,qBAAqB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAAA,IAC5E,QAAQ,MAAM,QAAQ,MAAM;AAAA,EAC9B,CAAC;AAAA,EACD,iBAAiB,SAAS;AAAA,IACxB,IAAI;AAAA,IACJ,MAAM;AAAA,EACR,CAAC;AAAA,EACD,WAAW,MAAM,QAAQ,QAAQ,MAAM,CAAC;AAAA,EACxC,eAAe,QAAQ,MAAM;AAE/B,CAAC;AAIM,IAAM,WAAgB,QAAQ,GAAG,CAAC,mBAAmB,oBAAoB,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;;;AC5CxG,eAAI,2BAA2B;AAAA,EAC7B,MAAM;AAAA,EACN,UAAU;AAAA,IAER,UAAU,SAAS;AAAA,MACjB,aAAa;AAAA,IACf,CAAC;AAAA,IACD,SAAU;AACR,aAAO;AAAA,QACL,aAAa,IAAI,KAAK,EAAE,YAAY;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,wBAAwB;AAAA,MACtB,UAAU;AAAA,MACV,QAAS,EAAE,QAAQ,EAAE,SAAS;AAC5B,mBAAW,OAAO,MAAM;AACtB,mBAAI,IAAI,OAAO,KAAK,KAAK,IAAI;AAAA,QAC/B;AACA,iBAAI,IAAI,OAAO,YAAY,CAAC,CAAC;AAAA,MAC/B;AAAA,IACF;AAAA,IACA,oCAAoC;AAAA,MAClC,UAAU,SAAS;AAAA,QACjB,aAAa;AAAA,QACb,MAAM;AAAA,QACN,SAAS,SAAS,MAAM;AAAA,QACxB,SAAS,SAAS,MAAM;AAAA,QACxB,SAAS,SAAS,MAAM;AAAA,MAC1B,CAAC;AAAA,MACD,QAAS,SAAS,EAAE,SAAS;AAC3B,cAAM,SAAS,KAAK,OAAO;AAAA,MAC7B;AAAA,IACF;AAAA,IACA,wCAAwC;AAAA,MACtC,UAAU,SAAS;AAAA,QACjB,QAAQ;AAAA,MACV,CAAC;AAAA,MACD,QAAS,EAAE,QAAQ,EAAE,SAAS;AAE5B,cAAM,IAAI,MAAM,gBAAgB;AAAA,MAClC;AAAA,IACF;AAAA,EACF;AACF,CAAC;", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj /*: Object */, fn /*: Function */, o /*: Object */ = {}) /*: any */ {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj /*: Object */, fn /*: Function */) /*: {[any]: any} */ {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o /*: Object */, props /*: string[] */) /*: Object */ {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o /*: Object */, where /*: Function */) /*: Object */ {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array /*: Array<*> */, indices /*: Array */) /*: Array<*> */ {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o /*: Object */, props /*: string[] */) /*: {...} */ {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj /*: Object */) /*: any */ {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj /*: Object */, src /*: Object */) /*: any */ {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec /*: number */) /*: Promise */ {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length /*: number */) /*: Uint8Array */ {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length /*: number */) /*: string */ {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min /*: number */, max /*: number */) /*: number */ {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr /*: any[] */) /*: any */ {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr /*: Array<*> */) /*: Array */ {\n let flat /*: Array<*> */ = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () /*: any[] */ {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array /*: any[] */) /*: any[] */ {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a /*: any */, b /*: any */) /*: boolean */ {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func /*: Function */, wait /*: number */, immediate /*: ?boolean */) /*: Function */ {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n\n/**\n * Gets the value at `path` of `obj`. If the resolved value is\n * `undefined`, the `defaultValue` is returned in its place.\n *\n */\nexport function get (obj /*: Object */, path /*: string[] */, defaultValue /*: any */) /*: any */ {\n if (!path.length) {\n return obj\n } else if (obj === undefined) {\n return defaultValue\n }\n\n let result = obj\n let i = 0\n while (result && i < path.length) {\n result = result[path[i]]\n i++\n }\n\n return result === undefined ? defaultValue : result\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport {\n objectOf, objectMaybeOf, arrayOf, unionOf,\n string, number, optional, mapOf, literalOf\n} from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,\n MESSAGE_TYPES, MESSAGE_NOTIFICATIONS,\n MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ\n} from './constants.js'\n\n// group.js related\n\nexport const inviteType = objectOf({\n inviteSecret: string,\n quantity: number,\n creator: string,\n invitee: optional(string),\n status: string,\n responses: mapOf(string, string),\n expires: number\n})\n\n// chatroom.js related\n\nexport const chatRoomAttributesType = objectOf({\n name: string,\n description: string,\n type: unionOf(...Object.values(CHATROOM_TYPES).map(v => literalOf(v))),\n privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map(v => literalOf(v)))\n})\n\nexport const messageType = objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),\n text: string, // message text | proposalId when type is INTERACTIVE | notificationType when type if NOTIFICATION\n notification: objectMaybeOf({\n type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),\n params: mapOf(string, string) // { username }\n }),\n replyingMessage: objectOf({\n id: string, // scroll to the original message and highlight\n text: string // display text(if too long, truncate)\n }),\n emoticons: mapOf(string, arrayOf(string)), // mapping of emoticons and usernames\n onlyVisibleTo: arrayOf(string) // list of usernames, only necessary when type is NOTIFICATION\n // TODO: need to consider POLL and add more down here\n})\n\n// mailbox.js related\n\nexport const mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map(k => literalOf(k)))\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { mailType } from './shared/types.js'\nimport { objectOf, string, object, optional } from '~/frontend/model/contracts/misc/flowTyper.js'\n\nsbp('chelonia/defineContract', {\n name: 'gi.contracts/mailbox',\n metadata: {\n // TODO: why is this missing the from username..?\n validate: objectOf({\n createdDate: string\n }),\n create () {\n return {\n createdDate: new Date().toISOString()\n }\n }\n },\n actions: {\n 'gi.contracts/mailbox': {\n validate: object, // TODO: define this\n process ({ data }, { state }) {\n for (const key in data) {\n Vue.set(state, key, data[key])\n }\n Vue.set(state, 'messages', [])\n }\n },\n 'gi.contracts/mailbox/postMessage': {\n validate: objectOf({\n messageType: mailType,\n from: string,\n subject: optional(string),\n message: optional(string),\n headers: optional(object)\n }),\n process (message, { state }) {\n state.messages.push(message)\n }\n },\n 'gi.contracts/mailbox/authorizeSender': {\n validate: objectOf({\n sender: string\n }),\n process ({ data }, { state }) {\n // TODO: replace this via OP_KEY_*?\n throw new Error('unimplemented!')\n }\n }\n }\n})\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/shared/functions.js.map b/test/contracts/shared/functions.js.map index 7df5c9f150..ab2ac265ed 100644 --- a/test/contracts/shared/functions.js.map +++ b/test/contracts/shared/functions.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../../node_modules/@sbp/sbp/dist/module.mjs", "../../../frontend/model/contracts/shared/constants.js", "../../../frontend/common/common.js", "../../../frontend/common/vSafeHtml.js", "../../../frontend/model/contracts/shared/giLodash.js", "../../../frontend/common/translations.js", "../../../frontend/common/stringTemplate.js", "../../../frontend/model/contracts/shared/time.js", "../../../frontend/views/utils/misc.js", "../../../frontend/model/contracts/shared/functions.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nexport function logExceptNavigationDuplicated (err ) {\n err.name !== 'NavigationDuplicated' && console.error(err)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { INVITE_STATUS, MESSAGE_TYPES } from './constants.js'\nimport { DAYS_MILLIS } from './time.js'\nimport { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js'\n\n// !!!!!!!!!!!!!!!\n// !! IMPORTANT !!\n// !!!!!!!!!!!!!!!\n//\n// DO NOT CHANGE THE LOGIC TO ANY OF THESE FUNCTIONS!\n// INSTEAD, CREATE NEW FUNCTIONS WITH DIFFERENT NAMES\n// AND USE THOSE INSTEAD!\n//\n// THIS IS A CONSEQUENCE OF SHARING THIS CODE WITH THE REST OF THE APP.\n// IF YOU DO NOT NEED TO SHARE CODE WITH THE REST OF THE APP (AND CAN\n// KEEP IT WITHIN THE CONTRACT ONLY), THEN YOU DON'T NEED TO WORRY ABOUT\n// THIS, AND SHOULD INCLUDE THOSE FUNCTIONS (WITHOUT EXPORTING THEM),\n// DIRECTLY IN YOUR CONTRACT DEFINITION FILE. THEN YOU CAN MODIFY\n// THEM AS MUCH AS YOU LIKE (and generate new contract versions out of them).\n\n// group.js related\n\nexport function createInvite ({ quantity = 1, creator, expires, invitee } \n \n ) \n \n \n \n \n \n \n \n {\n return {\n inviteSecret: `${parseInt(Math.random() * 10000)}`, // TODO: this\n quantity,\n creator,\n invitee,\n status: INVITE_STATUS.VALID,\n responses: {}, // { bob: true } list of usernames that accepted the invite.\n expires: Date.now() + DAYS_MILLIS * expires\n }\n}\n\n// chatroom.js related\n\nexport function createMessage ({ meta, data, hash, state } \n \n ) {\n const { type, text, replyingMessage } = data\n const { createdDate } = meta\n\n let newMessage = {\n type,\n datetime: new Date(createdDate).toISOString(),\n id: hash,\n from: meta.username\n }\n\n if (type === MESSAGE_TYPES.TEXT) {\n newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }\n } else if (type === MESSAGE_TYPES.POLL) {\n // TODO: Poll message creation\n } else if (type === MESSAGE_TYPES.NOTIFICATION) {\n const params = {\n channelName: state?.attributes.name,\n channelDescription: state?.attributes.description,\n ...data.notification\n }\n delete params.type\n newMessage = {\n ...newMessage,\n notification: { type: data.notification.type, params }\n }\n } else if (type === MESSAGE_TYPES.INTERACTIVE) {\n // TODO: Interactive message creation for proposals\n }\n return newMessage\n}\n\nexport async function leaveChatRoom ({ contractID } \n \n ) {\n const rootState = sbp('state/vuex/state')\n const rootGetters = sbp('state/vuex/getters')\n if (contractID === rootGetters.currentChatRoomId) {\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {\n groupId: rootState.currentGroupId\n })\n const curRouteName = sbp('controller/router').history.current.name\n if (curRouteName === 'GroupChat' || curRouteName === 'GroupChatConversation') {\n await sbp('controller/router')\n .push({ name: 'GroupChatConversation', params: { chatRoomId: rootGetters.currentChatRoomId } })\n .catch(logExceptNavigationDuplicated)\n }\n }\n\n sbp('state/vuex/commit', 'deleteChatRoomUnread', { chatRoomId: contractID })\n sbp('state/vuex/commit', 'deleteChatRoomScrollPosition', { chatRoomId: contractID })\n\n // NOTE: make sure *not* to await on this, since that can cause\n // a potential deadlock. See same warning in sideEffect for\n // 'gi.contracts/group/removeMember'\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e)\n })\n}\n\nexport function findMessageIdx (id , messages ) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].id === id) {\n return i\n }\n }\n return -1\n}\n\nexport function makeMentionFromUsername (username ) \n \n {\n return {\n me: `@${username}`,\n all: '@all'\n }\n}\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj /*: Object */, fn /*: Function */, o /*: Object */ = {}) /*: any */ {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj /*: Object */, fn /*: Function */) /*: {[any]: any} */ {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o /*: Object */, props /*: string[] */) /*: Object */ {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o /*: Object */, where /*: Function */) /*: Object */ {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array /*: Array<*> */, indices /*: Array */) /*: Array<*> */ {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o /*: Object */, props /*: string[] */) /*: {...} */ {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj /*: Object */) /*: any */ {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj /*: Object */, src /*: Object */) /*: any */ {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec /*: number */) /*: Promise */ {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length /*: number */) /*: Uint8Array */ {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length /*: number */) /*: string */ {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min /*: number */, max /*: number */) /*: number */ {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr /*: any[] */) /*: any */ {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr /*: Array<*> */) /*: Array */ {\n let flat /*: Array<*> */ = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () /*: any[] */ {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array /*: any[] */) /*: any[] */ {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a /*: any */, b /*: any */) /*: boolean */ {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func /*: Function */, wait /*: number */, immediate /*: ?boolean */) /*: Function */ {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n\n/**\n * Gets the value at `path` of `obj`. If the resolved value is\n * `undefined`, the `defaultValue` is returned in its place.\n *\n */\nexport function get (obj /*: Object */, path /*: string[] */, defaultValue /*: any */) /*: any */ {\n if (!path.length) {\n return obj\n } else if (obj === undefined) {\n return defaultValue\n }\n\n let result = obj\n let i = 0\n while (result && i < path.length) {\n result = result[path[i]]\n i++\n }\n\n return result === undefined ? defaultValue : result\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\nexport function logExceptNavigationDuplicated (err ) {\n err.name !== 'NavigationDuplicated' && console.error(err)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { INVITE_STATUS, MESSAGE_TYPES } from './constants.js'\nimport { DAYS_MILLIS } from './time.js'\nimport { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js'\n\n// !!!!!!!!!!!!!!!\n// !! IMPORTANT !!\n// !!!!!!!!!!!!!!!\n//\n// DO NOT CHANGE THE LOGIC TO ANY OF THESE FUNCTIONS!\n// INSTEAD, CREATE NEW FUNCTIONS WITH DIFFERENT NAMES\n// AND USE THOSE INSTEAD!\n//\n// THIS IS A CONSEQUENCE OF SHARING THIS CODE WITH THE REST OF THE APP.\n// IF YOU DO NOT NEED TO SHARE CODE WITH THE REST OF THE APP (AND CAN\n// KEEP IT WITHIN THE CONTRACT ONLY), THEN YOU DON'T NEED TO WORRY ABOUT\n// THIS, AND SHOULD INCLUDE THOSE FUNCTIONS (WITHOUT EXPORTING THEM),\n// DIRECTLY IN YOUR CONTRACT DEFINITION FILE. THEN YOU CAN MODIFY\n// THEM AS MUCH AS YOU LIKE (and generate new contract versions out of them).\n\n// group.js related\n\nexport function createInvite ({ quantity = 1, creator, expires, invitee } \n \n ) \n \n \n \n \n \n \n \n {\n return {\n inviteSecret: `${parseInt(Math.random() * 10000)}`, // TODO: this\n quantity,\n creator,\n invitee,\n status: INVITE_STATUS.VALID,\n responses: {}, // { bob: true } list of usernames that accepted the invite.\n expires: Date.now() + DAYS_MILLIS * expires\n }\n}\n\n// chatroom.js related\n\nexport function createMessage ({ meta, data, hash, state } \n \n ) {\n const { type, text, replyingMessage } = data\n const { createdDate } = meta\n\n let newMessage = {\n type,\n datetime: new Date(createdDate).toISOString(),\n id: hash,\n from: meta.username\n }\n\n if (type === MESSAGE_TYPES.TEXT) {\n newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }\n } else if (type === MESSAGE_TYPES.POLL) {\n // TODO: Poll message creation\n } else if (type === MESSAGE_TYPES.NOTIFICATION) {\n const params = {\n channelName: state?.attributes.name,\n channelDescription: state?.attributes.description,\n ...data.notification\n }\n delete params.type\n newMessage = {\n ...newMessage,\n notification: { type: data.notification.type, params }\n }\n } else if (type === MESSAGE_TYPES.INTERACTIVE) {\n // TODO: Interactive message creation for proposals\n }\n return newMessage\n}\n\nexport async function leaveChatRoom ({ contractID } \n \n ) {\n const rootState = sbp('state/vuex/state')\n const rootGetters = sbp('state/vuex/getters')\n if (contractID === rootGetters.currentChatRoomId) {\n sbp('state/vuex/commit', 'setCurrentChatRoomId', {\n groupId: rootState.currentGroupId\n })\n const curRouteName = sbp('controller/router').history.current.name\n if (curRouteName === 'GroupChat' || curRouteName === 'GroupChatConversation') {\n await sbp('controller/router')\n .push({ name: 'GroupChatConversation', params: { chatRoomId: rootGetters.currentChatRoomId } })\n .catch(logExceptNavigationDuplicated)\n }\n }\n\n sbp('state/vuex/commit', 'deleteChatRoomUnread', { chatRoomId: contractID })\n sbp('state/vuex/commit', 'deleteChatRoomScrollPosition', { chatRoomId: contractID })\n\n // NOTE: make sure *not* to await on this, since that can cause\n // a potential deadlock. See same warning in sideEffect for\n // 'gi.contracts/group/removeMember'\n sbp('chelonia/contract/remove', contractID).catch(e => {\n console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e)\n })\n}\n\nexport function findMessageIdx (id , messages ) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].id === id) {\n return i\n }\n }\n return -1\n}\n\nexport function makeMentionFromUsername (username ) \n \n {\n return {\n me: `@${username}`,\n all: '@all'\n }\n}\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/shared/giLodash.js b/test/contracts/shared/giLodash.js index 76d2910982..33e249bf68 100644 --- a/test/contracts/shared/giLodash.js +++ b/test/contracts/shared/giLodash.js @@ -176,6 +176,20 @@ function debounce(func, wait, immediate) { }; return debounced; } +function get(obj, path, defaultValue) { + if (!path.length) { + return obj; + } else if (obj === void 0) { + return defaultValue; + } + let result = obj; + let i = 0; + while (result && i < path.length) { + result = result[path[i]]; + i++; + } + return result === void 0 ? defaultValue : result; +} export { choose, cloneDeep, @@ -184,6 +198,7 @@ export { delay, difference, flatten, + get, intersection, mapObject, mapValues, diff --git a/test/contracts/shared/giLodash.js.map b/test/contracts/shared/giLodash.js.map index 044fb2479c..f16ea4d4fe 100644 --- a/test/contracts/shared/giLodash.js.map +++ b/test/contracts/shared/giLodash.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../../frontend/model/contracts/shared/giLodash.js"], - "sourcesContent": ["// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n"], - "mappings": "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", + "sourcesContent": ["// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj /*: Object */, fn /*: Function */, o /*: Object */ = {}) /*: any */ {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj /*: Object */, fn /*: Function */) /*: {[any]: any} */ {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o /*: Object */, props /*: string[] */) /*: Object */ {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o /*: Object */, where /*: Function */) /*: Object */ {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array /*: Array<*> */, indices /*: Array */) /*: Array<*> */ {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o /*: Object */, props /*: string[] */) /*: {...} */ {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj /*: Object */) /*: any */ {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj /*: Object */, src /*: Object */) /*: any */ {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec /*: number */) /*: Promise */ {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length /*: number */) /*: Uint8Array */ {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length /*: number */) /*: string */ {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min /*: number */, max /*: number */) /*: number */ {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr /*: any[] */) /*: any */ {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr /*: Array<*> */) /*: Array */ {\n let flat /*: Array<*> */ = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () /*: any[] */ {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array /*: any[] */) /*: any[] */ {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a /*: any */, b /*: any */) /*: boolean */ {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func /*: Function */, wait /*: number */, immediate /*: ?boolean */) /*: Function */ {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n\n/**\n * Gets the value at `path` of `obj`. If the resolved value is\n * `undefined`, the `defaultValue` is returned in its place.\n *\n */\nexport function get (obj /*: Object */, path /*: string[] */, defaultValue /*: any */) /*: any */ {\n if (!path.length) {\n return obj\n } else if (obj === undefined) {\n return defaultValue\n }\n\n let result = obj\n let i = 0\n while (result && i < path.length) {\n result = result[path[i]]\n i++\n }\n\n return result === undefined ? defaultValue : result\n}\n"], + "mappings": "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", "names": [] } diff --git a/test/contracts/shared/voting/proposals.js.map b/test/contracts/shared/voting/proposals.js.map index a791210663..2d49e9eb6a 100644 --- a/test/contracts/shared/voting/proposals.js.map +++ b/test/contracts/shared/voting/proposals.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../../../node_modules/@sbp/sbp/dist/module.mjs", "../../../../frontend/common/common.js", "../../../../frontend/common/vSafeHtml.js", "../../../../frontend/model/contracts/shared/giLodash.js", "../../../../frontend/common/translations.js", "../../../../frontend/common/stringTemplate.js", "../../../../frontend/model/contracts/misc/flowTyper.js", "../../../../frontend/model/contracts/shared/time.js", "../../../../frontend/model/contracts/shared/constants.js", "../../../../frontend/model/contracts/shared/voting/rules.js", "../../../../frontend/model/contracts/shared/voting/proposals.js"], - "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj, fn, o = {}) {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj, fn) {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o, props) {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o, where) {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array, indices) {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o, props) {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj) {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj, src) {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec) {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length) {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length) {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min, max) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr) {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr) {\n let flat = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array) {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays) {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1, ...arrays) {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1, ...arrays) {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a, b) {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func, wait, immediate) {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal ({ state, proposalHash, proposal, contractID }) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n"], - "mappings": "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", + "sourcesContent": ["// \n\n'use strict'\n\n\nconst selectors = {}\nconst domains = {}\nconst globalFilters = []\nconst domainFilters = {}\nconst selectorFilters = {}\nconst unsafeSelectors = {}\n\nconst DOMAIN_REGEX = /^[^/]+/\n\nfunction sbp (selector, ...data) {\n const domain = domainFromSelector(selector)\n if (!selectors[selector]) {\n throw new Error(`SBP: selector not registered: ${selector}`)\n }\n // Filters can perform additional functions, and by returning `false` they\n // can prevent the execution of a selector. Check the most specific filters first.\n for (const filters of [selectorFilters[selector], domainFilters[domain], globalFilters]) {\n if (filters) {\n for (const filter of filters) {\n if (filter(domain, selector, data) === false) return\n }\n }\n }\n return selectors[selector].call(domains[domain].state, ...data)\n}\n\nexport function domainFromSelector (selector) {\n const domainLookup = DOMAIN_REGEX.exec(selector)\n if (domainLookup === null) {\n throw new Error(`SBP: selector missing domain: ${selector}`)\n }\n return domainLookup[0]\n}\n\nconst SBP_BASE_SELECTORS = {\n 'sbp/selectors/register': function (sels) {\n const registered = []\n for (const selector in sels) {\n const domain = domainFromSelector(selector)\n if (selectors[selector]) {\n (console.warn || console.log)(`[SBP WARN]: not registering already registered selector: '${selector}'`)\n } else if (typeof sels[selector] === 'function') {\n if (unsafeSelectors[selector]) {\n // important warning in case we loaded any malware beforehand and aren't expecting this\n (console.warn || console.log)(`[SBP WARN]: registering unsafe selector: '${selector}' (remember to lock after overwriting)`)\n }\n const fn = selectors[selector] = sels[selector]\n registered.push(selector)\n // ensure each domain has a domain state associated with it\n if (!domains[domain]) {\n domains[domain] = { state: {} }\n }\n // call the special _init function immediately upon registering\n if (selector === `${domain}/_init`) {\n fn.call(domains[domain].state)\n }\n }\n }\n return registered\n },\n 'sbp/selectors/unregister': function (sels) {\n for (const selector of sels) {\n if (!unsafeSelectors[selector]) {\n throw new Error(`SBP: can't unregister locked selector: ${selector}`)\n }\n delete selectors[selector]\n }\n },\n 'sbp/selectors/overwrite': function (sels) {\n sbp('sbp/selectors/unregister', Object.keys(sels))\n return sbp('sbp/selectors/register', sels)\n },\n 'sbp/selectors/unsafe': function (sels) {\n for (const selector of sels) {\n if (selectors[selector]) {\n throw new Error('unsafe must be called before registering selector')\n }\n unsafeSelectors[selector] = true\n }\n },\n 'sbp/selectors/lock': function (sels) {\n for (const selector of sels) {\n delete unsafeSelectors[selector]\n }\n },\n 'sbp/selectors/fn': function (sel) {\n return selectors[sel]\n },\n 'sbp/filters/global/add': function (filter) {\n globalFilters.push(filter)\n },\n 'sbp/filters/domain/add': function (domain, filter) {\n if (!domainFilters[domain]) domainFilters[domain] = []\n domainFilters[domain].push(filter)\n },\n 'sbp/filters/selector/add': function (selector, filter) {\n if (!selectorFilters[selector]) selectorFilters[selector] = []\n selectorFilters[selector].push(filter)\n }\n}\n\nSBP_BASE_SELECTORS['sbp/selectors/register'](SBP_BASE_SELECTORS)\n\nexport default sbp\n", "'use strict'\n\n// This file allows us to deduplicate some code between the main app bundle and\n// the slim'd down contract bundles.\n// Any large shared dependencies between the contracts - that are unlikely to ever\n// change - go into this file. For example, we are using Vue between the frontend\n// and the contracts (for the Vue.set/Vue.delete functions), and those are unlikely\n// to ever change in their behavior. Therefore it's a perfect candidate to go in\n// this file, resulting a smaller overall bundle for the contract slim builds.\n// All other in-project code that's shared between the contracts should be\n// placed under 'frontend/model/contracts/shared/' and imported directly\n// from both the app and the contracts. This will result in some duplicated code being\n// loaded by the contracts (even the slim'd versions) and the main app, however,\n// it will ensure the safe and consistent operation of contracts, minimizing the\n// likelihood that there will ever be a conflict between their state and what the UI\n// expects the behavior to be.\n\n// EVERYTHING EXPORTED BY THIS FILE MUST ALWAYS AND ONLY BE IMPORTED\n// VIA \"/assets/js/common.js\"!\n// DO NOT CHANGE THE BEHAVIOR OF ANYTHING IMPORTED BY THIS FILE THAT AFFECTS THE\n// BEHAVIOR OF CONTRACTS IN ANY MEANINGFUL WAY!\n// Doing otherwise defeats the purpose of this file and could lead to bugs and conflicts!\n// You may *add* behavior, but never modify or remove it.\n\nexport { default as Vue } from 'vue'\nexport { default as L } from './translations.js'\nexport * from './translations.js'\nexport * from './errors.js'\nexport * as Errors from './errors.js'\n", "/*\n * Copyright GitLab B.V.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/*\n * This file is mainly a copy of the `safe-html.js` directive found in the\n * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project,\n * slightly modifed for linting purposes and to immediately register it via\n * `Vue.directive()` rather than exporting it, consistently with our other\n * custom Vue directives.\n */\n\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'\n\n// See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\nexport const defaultConfig = {\n ALLOWED_ATTR: ['class'],\n ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n // This option was in the original file.\n RETURN_DOM_FRAGMENT: true\n}\n\nconst transform = (el, binding) => {\n if (binding.oldValue !== binding.value) {\n let config = defaultConfig\n if (binding.arg === 'a') {\n config = cloneDeep(config)\n config.ALLOWED_ATTR.push('href', 'target')\n config.ALLOWED_TAGS.push('a')\n }\n el.textContent = ''\n el.appendChild(dompurify.sanitize(binding.value, config))\n }\n}\n\n/*\n * Register a global custom directive called `v-safe-html`.\n *\n * Please use this instead of `v-html` everywhere user input is expected,\n * in order to avoid XSS vulnerabilities.\n *\n * See https://github.com/okTurtles/group-income/issues/975\n */\n\nVue.directive('safe-html', {\n bind: transform,\n update: transform\n})\n", "// manually implemented lodash functions are better than even:\n// https://github.com/lodash/babel-plugin-lodash\n// additional tiny versions of lodash functions are available in VueScript2\n\nexport function mapValues (obj /*: Object */, fn /*: Function */, o /*: Object */ = {}) /*: any */ {\n for (const key in obj) { o[key] = fn(obj[key]) }\n return o\n}\n\nexport function mapObject (obj /*: Object */, fn /*: Function */) /*: {[any]: any} */ {\n return Object.fromEntries(Object.entries(obj).map(fn))\n}\n\nexport function pick (o /*: Object */, props /*: string[] */) /*: Object */ {\n const x = {}\n for (const k of props) { x[k] = o[k] }\n return x\n}\n\nexport function pickWhere (o /*: Object */, where /*: Function */) /*: Object */ {\n const x = {}\n for (const k in o) {\n if (where(o[k])) { x[k] = o[k] }\n }\n return x\n}\n\nexport function choose (array /*: Array<*> */, indices /*: Array */) /*: Array<*> */ {\n const x = []\n for (const idx of indices) { x.push(array[idx]) }\n return x\n}\n\nexport function omit (o /*: Object */, props /*: string[] */) /*: {...} */ {\n const x = {}\n for (const k in o) {\n if (!props.includes(k)) {\n x[k] = o[k]\n }\n }\n return x\n}\n\nexport function cloneDeep (obj /*: Object */) /*: any */ {\n return JSON.parse(JSON.stringify(obj))\n}\n\nfunction isMergeableObject (val) {\n const nonNullObject = val && typeof val === 'object'\n // $FlowFixMe\n return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'\n}\n\nexport function merge (obj /*: Object */, src /*: Object */) /*: any */ {\n for (const key in src) {\n const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : undefined\n if (clone && isMergeableObject(obj[key])) {\n merge(obj[key], clone)\n continue\n }\n obj[key] = clone || src[key]\n }\n return obj\n}\n\nexport function delay (msec /*: number */) /*: Promise */ {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, msec)\n })\n}\n\nexport function randomBytes (length /*: number */) /*: Uint8Array */ {\n // $FlowIssue crypto support: https://github.com/facebook/flow/issues/5019\n return crypto.getRandomValues(new Uint8Array(length))\n}\n\nexport function randomHexString (length /*: number */) /*: string */ {\n return Array.from(randomBytes(length), byte => (byte % 16).toString(16)).join('')\n}\n\nexport function randomIntFromRange (min /*: number */, max /*: number */) /*: number */ {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\nexport function randomFromArray (arr /*: any[] */) /*: any */ {\n return arr[Math.floor(Math.random() * arr.length)]\n}\n\nexport function flatten (arr /*: Array<*> */) /*: Array */ {\n let flat /*: Array<*> */ = []\n for (let i = 0; i < arr.length; i++) {\n if (Array.isArray(arr[i])) {\n flat = flat.concat(arr[i])\n } else {\n flat.push(arr[i])\n }\n }\n return flat\n}\n\nexport function zip () /*: any[] */ {\n // $FlowFixMe\n const arr = Array.prototype.slice.call(arguments)\n const zipped = []\n let max = 0\n arr.forEach((current) => (max = Math.max(max, current.length)))\n for (const current of arr) {\n for (let i = 0; i < max; i++) {\n zipped[i] = zipped[i] || []\n zipped[i].push(current[i])\n }\n }\n return zipped\n}\n\nexport function uniq (array /*: any[] */) /*: any[] */ {\n return Array.from(new Set(array))\n}\n\nexport function union (...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n return uniq([].concat.apply([], arrays))\n}\n\nexport function intersection (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n return uniq(a1).filter(v1 => arrays.every(v2 => v2.indexOf(v1) >= 0))\n}\n\nexport function difference (a1 /*: any[] */, ...arrays /*: any[][] */) /*: any[] */ {\n // $FlowFixMe\n const a2 = [].concat.apply([], arrays)\n return a1.filter(v => a2.indexOf(v) === -1)\n}\n\nexport function deepEqualJSONType (a /*: any */, b /*: any */) /*: boolean */ {\n if (a === b) return true\n if (a === null || b === null || typeof (a) !== typeof (b)) return false\n if (typeof a !== 'object') return a === b\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false\n } else if (a.constructor.name !== 'Object') {\n throw new Error(`not JSON type: ${a}`)\n }\n for (const key in a) {\n if (!deepEqualJSONType(a[key], b[key])) return false\n }\n return true\n}\n\n/**\n * Modified version of: https://github.com/component/debounce/blob/master/index.js\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing. The function also has a property 'clear'\n * that is a function which will clear the timer to prevent previously scheduled executions.\n *\n * @source underscore.js\n * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/\n * @param {Function} function to wrap\n * @param {Number} timeout in ms (`100`)\n * @param {Boolean} whether to execute at the beginning (`false`)\n * @api public\n */\nexport function debounce (func /*: Function */, wait /*: number */, immediate /*: ?boolean */) /*: Function */ {\n let timeout, args, context, timestamp, result\n if (wait == null) wait = 100\n\n function later () {\n const last = Date.now() - timestamp\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last)\n } else {\n timeout = null\n if (!immediate) {\n result = func.apply(context, args)\n context = args = null\n }\n }\n }\n\n const debounced = function () {\n context = this\n args = arguments\n timestamp = Date.now()\n const callNow = immediate && !timeout\n if (!timeout) timeout = setTimeout(later, wait)\n if (callNow) {\n result = func.apply(context, args)\n context = args = null\n }\n\n return result\n }\n\n debounced.clear = function () {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n debounced.flush = function () {\n if (timeout) {\n result = func.apply(context, args)\n context = args = null\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return debounced\n}\n\n/**\n * Gets the value at `path` of `obj`. If the resolved value is\n * `undefined`, the `defaultValue` is returned in its place.\n *\n */\nexport function get (obj /*: Object */, path /*: string[] */, defaultValue /*: any */) /*: any */ {\n if (!path.length) {\n return obj\n } else if (obj === undefined) {\n return defaultValue\n }\n\n let result = obj\n let i = 0\n while (result && i < path.length) {\n result = result[path[i]]\n i++\n }\n\n return result === undefined ? defaultValue : result\n}\n", "'use strict'\n\n// since this file is loaded by common.js, we avoid circular imports and directly import\nimport sbp from '@sbp/sbp'\nimport { defaultConfig as defaultDompurifyConfig } from './vSafeHtml.js'\nimport dompurify from 'dompurify'\nimport Vue from 'vue'\nimport template from './stringTemplate.js'\n\nVue.prototype.L = L\nVue.prototype.LTags = LTags\n\nconst defaultLanguage = 'en-US'\nconst defaultLanguageCode = 'en'\nconst defaultTranslationTable = {}\n\n/**\n * Allow 'href' and 'target' attributes to avoid breaking our hyperlinks,\n * but keep sanitizing their values.\n * See https://github.com/cure53/DOMPurify#can-i-configure-dompurify\n */\nconst dompurifyConfig = {\n ...defaultDompurifyConfig,\n ALLOWED_ATTR: ['class', 'href', 'rel', 'target'],\n ALLOWED_TAGS: ['a', 'b', 'br', 'button', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'],\n RETURN_DOM_FRAGMENT: false\n}\n\nlet currentLanguage = defaultLanguage\nlet currentLanguageCode = defaultLanguage.split('-')[0]\nlet currentTranslationTable = defaultTranslationTable\n\n/**\n * Loads the translation file corresponding to a given language.\n *\n * @param language - A BPC-47 language tag like the value\n * of `navigator.language`.\n *\n * Language tags must be compared in a case-insensitive way (\u00A72.1.1.).\n *\n * @see https://tools.ietf.org/rfc/bcp/bcp47.txt\n */\nsbp('sbp/selectors/register', {\n 'translations/init': async function init (language ) {\n // A language code is usually the first part of a language tag.\n const [languageCode] = language.toLowerCase().split('-')\n\n // No need to do anything if the requested language is already in use.\n if (language.toLowerCase() === currentLanguage.toLowerCase()) return\n\n // We can also return early if only the language codes match,\n // since we don't have culture-specific translations yet.\n if (languageCode === currentLanguageCode) return\n\n // Avoid fetching any ressource if the requested language is the default one.\n if (languageCode === defaultLanguageCode) {\n currentLanguage = defaultLanguage\n currentLanguageCode = defaultLanguageCode\n currentTranslationTable = defaultTranslationTable\n return\n }\n try {\n currentTranslationTable = (await sbp('backend/translations/get', language)) || defaultTranslationTable\n\n // Only set `currentLanguage` if there was no error fetching the ressource.\n currentLanguage = language\n currentLanguageCode = languageCode\n } catch (error) {\n console.error(error)\n }\n }\n})\n\n/*\nExamples:\n\nSimple string:\n i18n Hello world\n\nString with variables:\n i18n(\n :args='{ name: ourUsername }'\n ) Hello {name}!\n\nString with HTML markup inside:\n i18n(\n :args='{ ...LTags(\"strong\", \"span\"), name: ourUsername }'\n ) Hello {strong_}{name}{_strong}, today it's a {span_}nice day{_span}!\n | or\n i18n(\n :args='{ ...LTags(\"span\"), name: \"${ourUsername}\" }'\n ) Hello {name}, today it's a {span_}nice day{_span}!\n\nString with Vue components inside:\n i18n(\n compile\n :args='{ r1: ``, r2: \"\"}'\n ) Go to {r1}login{r2} page.\n\n## When to use LTags or write html as part of the key?\n- Use LTags when they wrap a variable and raw text. Example:\n\n i18n(\n :args='{ count: 5, ...LTags(\"strong\") }'\n ) Invite {strong}{count} members{strong} to the party!\n\n- Write HTML when it wraps only the variable.\n-- That way translators don't need to worry about extra information.\n i18n(\n :args='{ count: \"5\" }'\n ) Invite {count} members to the party!\n*/\n\nexport function LTags (...tags ) {\n const o = {\n 'br_': '
'\n }\n for (const tag of tags) {\n o[`${tag}_`] = `<${tag}>`\n o[`_${tag}`] = ``\n }\n return o\n}\n\nexport default function L (\n key ,\n args \n) {\n return template(currentTranslationTable[key] || key, args)\n // Avoid inopportune linebreaks before certain punctuations.\n .replace(/\\s(?=[;:?!])/g, ' ')\n}\n\nexport function LError (error ) {\n let url = `/app/dashboard?modal=UserSettingsModal§ion=application-logs&errorMsg=${encodeURI(error.message)}`\n if (!sbp('state/vuex/state').loggedIn) {\n url = 'https://github.com/okTurtles/group-income/issues'\n }\n return {\n reportError: L('\"{errorMsg}\". You can {a_}report the error{_a}.', {\n errorMsg: error.message,\n 'a_': ``,\n '_a': ''\n })\n }\n}\n\nfunction sanitize (inputString) {\n return dompurify.sanitize(inputString, dompurifyConfig)\n}\n\nVue.component('i18n', {\n functional: true,\n props: {\n args: [Object, Array],\n tag: {\n type: String,\n default: 'span'\n },\n compile: Boolean\n },\n render: function (h, context) {\n const text = context.children[0].text\n const translation = L(text, context.props.args || {})\n if (!translation) {\n console.warn('The following i18n text was not translated correctly:', text)\n return h(context.props.tag, context.data, text)\n }\n // Prevent reverse tabnabbing by including `rel=\"noopener noreferrer\"` when rendering as an outbound hyperlink.\n if (context.props.tag === 'a' && context.data.attrs.target === '_blank') {\n context.data.attrs.rel = 'noopener noreferrer'\n }\n if (context.props.compile) {\n const result = Vue.compile('' + sanitize(translation) + '')\n // console.log('TRANSLATED RENDERED TEXT:', context, result.render.toString())\n return result.render.call({\n _c: (tag, ...args) => {\n if (tag === 'wrap') {\n return h(context.props.tag, context.data, ...args)\n } else {\n return h(tag, ...args)\n }\n },\n _v: x => x\n })\n } else {\n if (!context.data.domProps) context.data.domProps = {}\n context.data.domProps.innerHTML = sanitize(translation)\n return h(context.props.tag, context.data)\n }\n }\n})\n", "const nargs = /\\{([0-9a-zA-Z_]+)\\}/g\n\nexport default function template (string , ...args ) {\n const firstArg = args[0]\n // If the first rest argument is a plain object or array, use it as replacement table.\n // Otherwise, use the whole rest array as replacement table.\n const replacementsByKey = (\n (typeof firstArg === 'object' && firstArg !== null)\n ? firstArg\n : args\n )\n\n return string.replace(nargs, function replaceArg (match, capture, index) {\n // Avoid replacing the capture if it is escaped by double curly braces.\n if (string[index - 1] === '{' && string[index + match.length] === '}') {\n return capture\n }\n\n const maybeReplacement = (\n // Avoid accessing inherited properties of the replacement table.\n // $FlowFixMe\n Object.prototype.hasOwnProperty.call(replacementsByKey, capture)\n ? replacementsByKey[capture]\n : undefined\n )\n\n if (maybeReplacement === null || maybeReplacement === undefined) {\n return ''\n }\n return String(maybeReplacement)\n })\n}\n", "// to make rollup happy, I copied flowTyper-js\n// library into this file (it was refusing to\n// import because of the way functions were being\n// exported).\n//\n// GI EDIT NOTES:\n//\n// - The following functions can be used directly with 'validate'\n// because they've had their '_scope' second parameter removed:\n// - arrayOf\n// - mapOf\n// - object\n// - objectOf\n// - objectMaybeOf (this is a custom function that didn't exist in flowTyper)\n//\n// TODO: remove this file from eslintIgnore in package.json and fix errors\n\n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\nexport const EMPTY_VALUE = Symbol('@@empty')\nexport const isEmpty = v => v === EMPTY_VALUE\nexport const isNil = v => v === null\nexport const isUndef = v => typeof v === 'undefined'\nexport const isBoolean = v => typeof v === 'boolean'\nexport const isNumber = v => typeof v === 'number'\nexport const isString = v => typeof v === 'string'\nexport const isObject = v => !isNil(v) && typeof v === 'object'\nexport const isFunction = v => typeof v === 'function'\n\nexport const isType = typeFn => (v, _scope = '') => {\n try {\n typeFn(v, _scope)\n return true\n } catch (_) {\n return false\n }\n}\n\n// This function will return value based on schema with inferred types. This\n// value can be used to define type in Flow with 'typeof' utility.\nexport const typeOf = schema => schema(EMPTY_VALUE, '')\nexport const getType = (typeFn, _options) => {\n if (isFunction(typeFn.type)) return typeFn.type(_options)\n return typeFn.name || '?'\n}\n\n// error\nexport class TypeValidatorError extends Error {\n expectedType \n valueType \n value \n typeScope \n sourceFile \n\n constructor (\n message ,\n expectedType ,\n valueType ,\n value ,\n typeName = '',\n typeScope = ''\n ) {\n const errMessage = message ||\n `invalid \"${valueType}\" value type; ${typeName || expectedType} type expected`\n super(errMessage)\n this.expectedType = expectedType\n this.valueType = valueType\n this.value = value\n this.typeScope = typeScope || ''\n this.sourceFile = this.getSourceFile()\n this.message = `${errMessage}\\n${this.getErrorInfo()}`\n this.name = this.constructor.name\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TypeValidatorError)\n }\n }\n\n getSourceFile () {\n const fileNames = this.stack.match(/(\\/[\\w_\\-.]+)+(\\.\\w+:\\d+:\\d+)/g) || []\n return fileNames.find(fileName => fileName.indexOf('/flowTyper-js/dist/') === -1) || ''\n }\n\n getErrorInfo () {\n return `\n file ${this.sourceFile}\n scope ${this.typeScope}\n expected ${this.expectedType.replace(/\\n/g, '')}\n type ${this.valueType}\n value ${this.value}\n`\n }\n}\n\n// TypeValidatorError.prototype.name = 'TypeValidatorError'\n// exports.TypeValidatorError = TypeValidatorError\n\nconst validatorError = (\n typeFn ,\n value ,\n scope ,\n message ,\n expectedType ,\n valueType \n) => {\n return new TypeValidatorError(\n message,\n expectedType || getType(typeFn),\n valueType || typeof value,\n JSON.stringify(value),\n typeFn.name,\n scope\n )\n}\n\nexport const arrayOf =\n (typeFn , _scope = 'Array') => {\n function array (value) {\n if (isEmpty(value)) return [typeFn(value)]\n if (Array.isArray(value)) {\n let index = 0\n return value.map(v => typeFn(v, `${_scope}[${index++}]`))\n }\n throw validatorError(array, value, _scope)\n }\n array.type = () => `Array<${getType(typeFn)}>`\n return array\n }\n\nexport const literalOf =\n (primitive ) => {\n function literal (value, _scope = '') {\n if (isEmpty(value) || (value === primitive)) return primitive\n throw validatorError(literal, value, _scope)\n }\n literal.type = () => {\n if (isBoolean(primitive)) return `${primitive ? 'true' : 'false'}`\n else return `\"${primitive}\"`\n }\n return literal\n }\n\nexport const mapOf = (\n keyTypeFn ,\n typeFn \n) => {\n function mapOf (value) {\n if (isEmpty(value)) return {}\n const o = object(value)\n const reducer = (acc, key) =>\n Object.assign(\n acc,\n {\n // $FlowFixMe\n [keyTypeFn(key, 'Map[_]')]: typeFn(o[key], `Map.${key}`)\n }\n )\n return Object.keys(o).reduce(reducer, {})\n }\n mapOf.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`\n return mapOf\n}\n\nconst isPrimitiveFn = (typeName) =>\n ['undefined', 'null', 'boolean', 'number', 'string'].includes(typeName)\n\nexport const maybe =\n (typeFn ) => {\n function maybe (value, _scope = '') {\n return (isNil(value) || isUndef(value)) ? value : typeFn(value, _scope)\n }\n maybe.type = () => !isPrimitiveFn(typeFn.name) ? `?(${getType(typeFn)})` : `?${getType(typeFn)}`\n return maybe\n }\n\nexport const mixed = (\n function mixed (value) {\n return value\n } \n)\n\nexport const object = (\n function (value) {\n if (isEmpty(value)) return {}\n if (isObject(value) && !Array.isArray(value)) {\n return Object.assign({}, value)\n }\n throw validatorError(object, value)\n } \n)\n\nexport const objectOf = \n (typeObj , _scope = 'Object') => {\n function object2 (value) {\n const o = object(value)\n const typeAttrs = Object.keys(typeObj)\n const unknownAttr = Object.keys(o).find(attr => !typeAttrs.includes(attr))\n if (unknownAttr) {\n throw validatorError(\n object2,\n value,\n _scope,\n `missing object property '${unknownAttr}' in ${_scope} type`\n )\n }\n // IMPORTANT: because esbuild can actually rename the functions in the compiled source\n // (e.g. from 'optional' to 'optional2'), we use .includes() instead of ===\n // to check the .name of a function\n const undefAttr = typeAttrs.find(property => {\n const propertyTypeFn = typeObj[property]\n return (propertyTypeFn.name.includes('maybe') && !o.hasOwnProperty(property))\n })\n if (undefAttr) {\n throw validatorError(\n object2,\n o[undefAttr],\n `${_scope}.${undefAttr}`,\n `empty object property '${undefAttr}' for ${_scope} type`,\n `void | null | ${getType(typeObj[undefAttr]).substr(1)}`,\n '-'\n )\n }\n\n const reducer = isEmpty(value)\n ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) })\n : (acc, key) => {\n const typeFn = typeObj[key]\n if (typeFn.name.includes('optional') && !o.hasOwnProperty(key)) {\n return Object.assign(acc, {})\n } else {\n return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) })\n }\n }\n return typeAttrs.reduce(reducer, {})\n }\n object2.type = () => {\n const props = Object.keys(typeObj).map(\n (key) => {\n const ret = typeObj[key].name.includes('optional')\n ? `${key}?: ${getType(typeObj[key], { noVoid: true })}`\n : `${key}: ${getType(typeObj[key])}`\n return ret\n }\n )\n return `{|\\n ${props.join(',\\n ')} \\n|}`\n }\n return object2\n}\n\n// TODO: add flow type annotations and make it use validatorError etc.\nexport function objectMaybeOf (validations , _scope = 'Object') {\n return function (data ) {\n object(data)\n for (const key in data) {\n validations[key]?.(data[key], `${_scope}.${key}`)\n }\n return data\n }\n}\n\nexport const optional =\n (typeFn ) => {\n const unionFn = unionOf(typeFn, undef)\n function optional (v) {\n return unionFn(v)\n }\n optional.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn)\n return optional\n }\n\nexport const nil = (\n function nil (value) {\n if (isEmpty(value) || isNil(value)) return null\n throw validatorError(nil, value)\n }\n \n)\n\nexport function undef (value, _scope = '') {\n if (isEmpty(value) || isUndef(value)) return undefined\n throw validatorError(undef, value, _scope)\n}\nundef.type = () => 'void'\n// export const undef = (undef: TypeValidator)\n\nexport const boolean = (\n function boolean (value, _scope = '') {\n if (isEmpty(value)) return false\n if (isBoolean(value)) return value\n throw validatorError(boolean, value, _scope)\n }\n \n)\n\nexport const number = (\n function number (value, _scope = '') {\n if (isEmpty(value)) return 0\n if (isNumber(value)) return value\n throw validatorError(number, value, _scope)\n }\n \n)\n\nexport const string = (\n function string (value, _scope = '') {\n if (isEmpty(value)) return ''\n if (isString(value)) return value\n throw validatorError(string, value, _scope)\n }\n \n)\n\n \n \n \n \n \n \n \n \n \n \n \n \n\nfunction tupleOf_ (...typeFuncs) {\n function tuple (value , _scope = '') {\n const cardinality = typeFuncs.length\n if (isEmpty(value)) return typeFuncs.map(fn => fn(value))\n if (Array.isArray(value) && value.length === cardinality) {\n const tupleValue = []\n for (let i = 0; i < cardinality; i += 1) {\n tupleValue.push(typeFuncs[i](value[i], _scope))\n }\n return tupleValue\n }\n throw validatorError(tuple, value, _scope)\n }\n tuple.type = () => `[${typeFuncs.map(fn => getType(fn)).join(', ')}]`\n return tuple\n}\n\n// $FlowFixMe - $Tuple<(A, B, C, ...)[]>\n// const tupleOf: TupleT = tupleOf_\nexport const tupleOf = tupleOf_\n\n \n \n \n \n \n \n \n \n \n \n \n\nfunction unionOf_ (...typeFuncs) {\n function union (value , _scope = '') {\n for (const typeFn of typeFuncs) {\n try {\n return typeFn(value, _scope)\n } catch (_) {}\n }\n throw validatorError(union, value, _scope)\n }\n union.type = () => `(${typeFuncs.map(fn => getType(fn)).join(' | ')})`\n return union\n}\n// $FlowFixMe\n// const unionOf: UnionT = (unionOf_)\nexport const unionOf = unionOf_", "'use strict'\n\nimport { L } from '@common/common.js'\n\nexport const MINS_MILLIS = 60000\nexport const HOURS_MILLIS = 60 * MINS_MILLIS\nexport const DAYS_MILLIS = 24 * HOURS_MILLIS\nexport const MONTHS_MILLIS = 30 * DAYS_MILLIS\n\nexport function addMonthsToDate (date , months ) {\n const now = new Date(date)\n return new Date(now.setMonth(now.getMonth() + months))\n}\n\n// It might be tempting to deal directly with Dates and ISOStrings, since that's basically\n// what a period stamp is at the moment, but keeping this abstraction allows us to change\n// our mind in the future simply by editing these two functions.\n// TODO: We may want to, for example, get the time from the server instead of relying on\n// the client in case the client's clock isn't set correctly.\n// See: https://github.com/okTurtles/group-income/issues/531\nexport function dateToPeriodStamp (date ) {\n return new Date(date).toISOString()\n}\n\nexport function dateFromPeriodStamp (daystamp ) {\n return new Date(daystamp)\n}\n\nexport function periodStampGivenDate ({ recentDate, periodStart, periodLength } \n \n ) {\n const periodStartDate = dateFromPeriodStamp(periodStart)\n let nextPeriod = addTimeToDate(periodStartDate, periodLength)\n const curDate = new Date(recentDate)\n let curPeriod\n if (curDate < nextPeriod) {\n if (curDate >= periodStartDate) {\n return periodStart // we're still in the same period\n } else {\n // we're in a period before the current one\n curPeriod = periodStartDate\n do {\n curPeriod = addTimeToDate(curPeriod, -periodLength)\n } while (curDate < curPeriod)\n }\n } else {\n // we're at least a period ahead of periodStart\n do {\n curPeriod = nextPeriod\n nextPeriod = addTimeToDate(nextPeriod, periodLength)\n } while (curDate >= nextPeriod)\n }\n return dateToPeriodStamp(curPeriod)\n}\n\nexport function dateIsWithinPeriod ({ date, periodStart, periodLength } \n \n ) {\n const dateObj = new Date(date)\n const start = dateFromPeriodStamp(periodStart)\n return dateObj > start && dateObj < addTimeToDate(start, periodLength)\n}\n\nexport function addTimeToDate (date , timeMillis ) {\n const d = new Date(date)\n d.setTime(d.getTime() + timeMillis)\n return d\n}\n\nexport function dateToMonthstamp (date ) {\n // we could use Intl.DateTimeFormat but that doesn't support .format() on Android\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/format\n return new Date(date).toISOString().slice(0, 7)\n}\n\nexport function dateFromMonthstamp (monthstamp ) {\n // this is a hack to prevent new Date('2020-01').getFullYear() => 2019\n return new Date(`${monthstamp}-01T00:01:00.000Z`) // the Z is important\n}\n\n// TODO: to prevent conflicts among user timezones, we need\n// to use the server's time, and not our time here.\n// https://github.com/okTurtles/group-income/issues/531\nexport function currentMonthstamp () {\n return dateToMonthstamp(new Date())\n}\n\nexport function prevMonthstamp (monthstamp ) {\n const date = dateFromMonthstamp(monthstamp)\n date.setMonth(date.getMonth() - 1)\n return dateToMonthstamp(date)\n}\n\nexport function comparePeriodStamps (periodA , periodB ) {\n return dateFromPeriodStamp(periodA).getTime() - dateFromPeriodStamp(periodB).getTime()\n}\n\nexport function compareMonthstamps (monthstampA , monthstampB ) {\n return dateFromMonthstamp(monthstampA).getTime() - dateFromMonthstamp(monthstampB).getTime()\n // const A = dateA.getMonth() + dateA.getFullYear() * 12\n // const B = dateB.getMonth() + dateB.getFullYear() * 12\n // return A - B\n}\n\nexport function compareISOTimestamps (a , b ) {\n return new Date(a).getTime() - new Date(b).getTime()\n}\n\nexport function lastDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\nexport function firstDayOfMonth (date ) {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function humanDate (\n date ,\n options = { month: 'short', day: 'numeric' }\n) {\n const locale = typeof navigator === 'undefined'\n // Fallback for Mocha tests.\n ? 'en-US'\n // Flow considers `navigator.languages` to be of type `$ReadOnlyArray`,\n // which is not compatible with the `string[]` expected by `.toLocaleDateString()`.\n // Casting to `string[]` through `any` as a workaround.\n : ((navigator.languages ) ) ?? navigator.language\n // NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.\n // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString\n return new Date(date).toLocaleDateString(locale, options)\n}\n\nexport function isPeriodStamp (arg ) {\n return /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z/.test(arg)\n}\n\nexport function isFullMonthstamp (arg ) {\n return /^\\d{4}-(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function isMonthstamp (arg ) {\n return isShortMonthstamp(arg) || isFullMonthstamp(arg)\n}\n\nexport function isShortMonthstamp (arg ) {\n return /^(0[1-9]|1[0-2])$/.test(arg)\n}\n\nexport function monthName (monthstamp ) {\n return humanDate(dateFromMonthstamp(monthstamp), { month: 'long' })\n}\n\nexport function proximityDate (date ) {\n date = new Date(date)\n const today = new Date()\n const yesterday = (d => new Date(d.setDate(d.getDate() - 1)))(new Date())\n const lastWeek = (d => new Date(d.setDate(d.getDate() - 7)))(new Date())\n\n for (const toReset of [date, today, yesterday, lastWeek]) {\n toReset.setHours(0)\n toReset.setMinutes(0)\n toReset.setSeconds(0, 0)\n }\n\n const datems = Number(date)\n let pd = date > lastWeek ? humanDate(datems, { month: 'short', day: 'numeric', year: 'numeric' }) : humanDate(datems)\n if (date.getTime() === yesterday.getTime()) pd = L('Yesterday')\n if (date.getTime() === today.getTime()) pd = L('Today')\n\n return pd\n}\n\nexport function timeSince (datems , dateNow = Date.now()) {\n const interval = dateNow - datems\n\n if (interval >= DAYS_MILLIS * 2) {\n // Make sure to replace any ordinary space character by a non-breaking one.\n return humanDate(datems).replace(/\\x32/g, '\\xa0')\n }\n if (interval >= DAYS_MILLIS) {\n return L('1d')\n }\n if (interval >= HOURS_MILLIS) {\n return L('{hours}h', { hours: Math.floor(interval / HOURS_MILLIS) })\n }\n if (interval >= MINS_MILLIS) {\n // Maybe use 'min' symbol rather than 'm'?\n return L('{minutes}m', { minutes: Math.max(1, Math.floor(interval / MINS_MILLIS)) })\n }\n return L('<1m')\n}\n\nexport function cycleAtDate (atDate ) {\n const now = new Date(atDate) // Just in case the parameter is a string type.\n const partialCycles = now.getDate() / lastDayOfMonth(now).getDate()\n return partialCycles\n}\n", "'use strict'\n\n// identity.js related\n\nexport const IDENTITY_PASSWORD_MIN_CHARS = 7\nexport const IDENTITY_USERNAME_MAX_CHARS = 80\n\n// group.js related\n\nexport const INVITE_INITIAL_CREATOR = 'invite-initial-creator'\nexport const INVITE_STATUS = {\n REVOKED: 'revoked',\n VALID: 'valid',\n USED: 'used'\n}\nexport const PROFILE_STATUS = {\n ACTIVE: 'active', // confirmed group join\n PENDING: 'pending', // shortly after being approved to join the group\n REMOVED: 'removed'\n}\n\nexport const PROPOSAL_RESULT = 'proposal-result'\nexport const PROPOSAL_INVITE_MEMBER = 'invite-member'\nexport const PROPOSAL_REMOVE_MEMBER = 'remove-member'\nexport const PROPOSAL_GROUP_SETTING_CHANGE = 'group-setting-change'\nexport const PROPOSAL_PROPOSAL_SETTING_CHANGE = 'proposal-setting-change'\nexport const PROPOSAL_GENERIC = 'generic'\n\nexport const PROPOSAL_ARCHIVED = 'proposal-archived'\nexport const MAX_ARCHIVED_PROPOSALS = 100\n\nexport const STATUS_OPEN = 'open'\nexport const STATUS_PASSED = 'passed'\nexport const STATUS_FAILED = 'failed'\nexport const STATUS_EXPIRED = 'expired'\nexport const STATUS_CANCELLED = 'cancelled'\n\n// chatroom.js related\n\nexport const CHATROOM_GENERAL_NAME = 'General'\nexport const CHATROOM_NAME_LIMITS_IN_CHARS = 50\nexport const CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280\nexport const CHATROOM_ACTIONS_PER_PAGE = 40\nexport const CHATROOM_MESSAGES_PER_PAGE = 20\n\n// chatroom events\nexport const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'\nexport const CHATROOM_DETAILS_UPDATED = 'chatroom-details-updated'\nexport const MESSAGE_RECEIVE = 'message-receive'\nexport const MESSAGE_SEND = 'message-send'\n\nexport const CHATROOM_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group'\n}\n\nexport const CHATROOM_PRIVACY_LEVEL = {\n GROUP: 'chatroom-privacy-level-group',\n PRIVATE: 'chatroom-privacy-level-private',\n PUBLIC: 'chatroom-privacy-level-public'\n}\n\nexport const MESSAGE_TYPES = {\n POLL: 'message-poll',\n TEXT: 'message-text',\n INTERACTIVE: 'message-interactive',\n NOTIFICATION: 'message-notification'\n}\n\nexport const INVITE_EXPIRES_IN_DAYS = {\n ON_BOARDING: 30,\n PROPOSAL: 7\n}\n\nexport const MESSAGE_NOTIFICATIONS = {\n ADD_MEMBER: 'add-member',\n JOIN_MEMBER: 'join-member',\n LEAVE_MEMBER: 'leave-member',\n KICK_MEMBER: 'kick-member',\n UPDATE_DESCRIPTION: 'update-description',\n UPDATE_NAME: 'update-name',\n DELETE_CHANNEL: 'delete-channel',\n VOTE: 'vote'\n}\n\nexport const MESSAGE_VARIANTS = {\n PENDING: 'pending',\n SENT: 'sent',\n RECEIVED: 'received',\n FAILED: 'failed'\n}\n\n// mailbox.js related\n\nexport const MAIL_TYPE_MESSAGE = 'message'\nexport const MAIL_TYPE_FRIEND_REQ = 'friend-request'\n", "'use strict'\n\nimport { literalOf, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport {\n PROPOSAL_REMOVE_MEMBER,\n PROFILE_STATUS\n} from '../constants.js'\n\nexport const VOTE_AGAINST = ':against'\nexport const VOTE_INDIFFERENT = ':indifferent'\nexport const VOTE_UNDECIDED = ':undecided'\nexport const VOTE_FOR = ':for'\n\nexport const RULE_PERCENTAGE = 'percentage'\nexport const RULE_DISAGREEMENT = 'disagreement'\nexport const RULE_MULTI_CHOICE = 'multi-choice'\n\n// TODO: ranked-choice? :D\n\n/* REVIVEW PR\n the whole \"state\" being passed as argument to rules[rule]() is overwhelmed.\n It would be simpler with just 2 simple args: \"threshold\" and \"population\".\n Advantages:\n - population: No need to do the logic to get it (and avoid import PROFILE_STATUS.ACTIVE from group.js).\n - threshold: The selector to get the selector is huge.\n - tests: Avoid the need to mock a complex state.\n My suggestion: 1 parameter (object) with 4 explicit keys.\n [RULE_PERCENTAGE]: function ({ votes, proposalType, population, threshold })\n*/\n\nconst getPopulation = (state) => Object.keys(state.profiles).filter(p => state.profiles[p].status === PROFILE_STATUS.ACTIVE).length\n\nconst rules = {\n [RULE_PERCENTAGE]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n let population = getPopulation(state)\n if (proposalType === PROPOSAL_REMOVE_MEMBER) population -= 1\n const defaultThreshold = state.settings.proposals[proposalType].ruleSettings[RULE_PERCENTAGE].threshold\n const threshold = getThresholdAdjusted(RULE_PERCENTAGE, defaultThreshold, population)\n const totalIndifferent = votes.filter(x => x === VOTE_INDIFFERENT).length\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const totalForOrAgainst = totalFor + totalAgainst\n const turnout = totalForOrAgainst + totalIndifferent\n const absent = population - turnout\n // TODO: figure out if this is the right way to figure out the \"neededToPass\" number\n // and if so, explain it in the UI that the threshold is applied only to\n // *those who care, plus those who were abscent*.\n // Those who explicitely say they don't care are removed from consideration.\n const neededToPass = Math.ceil(threshold * (population - totalIndifferent))\n console.debug(`votingRule ${RULE_PERCENTAGE} for ${proposalType}:`, { neededToPass, totalFor, totalAgainst, totalIndifferent, threshold, absent, turnout, population })\n if (totalFor >= neededToPass) {\n return VOTE_FOR\n }\n return totalFor + absent < neededToPass ? VOTE_AGAINST : VOTE_UNDECIDED\n },\n [RULE_DISAGREEMENT]: function (state, proposalType, votes) {\n votes = Object.values(votes)\n const population = getPopulation(state)\n const minimumMax = proposalType === PROPOSAL_REMOVE_MEMBER ? 2 : 1\n const thresholdOriginal = Math.max(state.settings.proposals[proposalType].ruleSettings[RULE_DISAGREEMENT].threshold, minimumMax)\n const threshold = getThresholdAdjusted(RULE_DISAGREEMENT, thresholdOriginal, population)\n const totalFor = votes.filter(x => x === VOTE_FOR).length\n const totalAgainst = votes.filter(x => x === VOTE_AGAINST).length\n const turnout = votes.length\n const absent = population - turnout\n\n console.debug(`votingRule ${RULE_DISAGREEMENT} for ${proposalType}:`, { totalFor, totalAgainst, threshold, turnout, population, absent })\n if (totalAgainst >= threshold) {\n return VOTE_AGAINST\n }\n // consider proposal passed if more vote for it than against it and there aren't\n // enough votes left to tip the scales past the threshold\n return totalAgainst + absent < threshold ? VOTE_FOR : VOTE_UNDECIDED\n },\n [RULE_MULTI_CHOICE]: function (state, proposalType, votes) {\n throw new Error('unimplemented!')\n // TODO: return VOTE_UNDECIDED if 0 votes, otherwise value w/greatest number of votes\n // the proposal/poll is considered passed only after the expiry time period\n // NOTE: are we sure though that this even belongs here as a voting rule...?\n // perhaps there could be a situation were one of several valid settings options\n // is being proposed... in effect this would be a plurality voting rule\n }\n}\n\nexport default rules\n\nexport const ruleType = unionOf(...Object.keys(rules).map(k => literalOf(k)))\n\n/**\n *\n * @example ('disagreement', 2, 1) => 2\n * @example ('disagreement', 5, 1) => 3\n * @example ('disagreement', 7, 10) => 7\n * @example ('disagreement', 20, 10) => 10\n *\n * @example ('percentage', 0.5, 3) => 0.5\n * @example ('percentage', 0.1, 3) => 0.33\n * @example ('percentage', 0.1, 10) => 0.2\n * @example ('percentage', 0.3, 10) => 0.3\n */\nexport const getThresholdAdjusted = (rule , threshold , groupSize ) => {\n const groupSizeVoting = Math.max(3, groupSize) // 3 = minimum groupSize to vote\n\n return {\n [RULE_DISAGREEMENT]: () => {\n // Limit number of maximum \"no\" votes to group size\n return Math.min(groupSizeVoting - 1, threshold)\n },\n [RULE_PERCENTAGE]: () => {\n // Minimum threshold correspondent to 2 \"yes\" votes\n const minThreshold = 2 / groupSizeVoting\n return Math.max(minThreshold, threshold)\n }\n }[rule]()\n}\n\n/**\n *\n * @example (10, 0.5) => 5\n * @example (3, 0.8) => 3\n * @example (1, 0.6) => 2\n */\nexport const getCountOutOfMembers = (groupSize , decimal ) => {\n const minGroupSize = 3 // when group can vote\n return Math.ceil(Math.max(minGroupSize, groupSize) * decimal)\n}\n\nexport const getPercentFromDecimal = (decimal ) => {\n // convert decimal to percentage avoiding weird decimals results.\n // e.g. 0.58 -> 58 instead of 57.99999\n return Math.round(decimal * 100)\n}\n", "'use strict'\n\nimport sbp from '@sbp/sbp'\nimport { Vue } from '@common/common.js'\nimport { objectOf, literalOf, unionOf, number } from '~/frontend/model/contracts/misc/flowTyper.js'\nimport { DAYS_MILLIS } from '../time.js'\nimport rules, { ruleType, VOTE_UNDECIDED, VOTE_AGAINST, VOTE_FOR, RULE_PERCENTAGE, RULE_DISAGREEMENT } from './rules.js'\nimport {\n PROPOSAL_RESULT,\n PROPOSAL_INVITE_MEMBER,\n PROPOSAL_REMOVE_MEMBER,\n PROPOSAL_GROUP_SETTING_CHANGE,\n PROPOSAL_PROPOSAL_SETTING_CHANGE,\n PROPOSAL_GENERIC,\n // STATUS_OPEN,\n STATUS_PASSED,\n STATUS_FAILED\n // STATUS_EXPIRED,\n // STATUS_CANCELLED\n} from '../constants.js'\n\nexport function archiveProposal (\n { state, proposalHash, proposal, contractID } \n \n ) {\n Vue.delete(state.proposals, proposalHash)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/archiveProposal', contractID, proposalHash, proposal]\n )\n}\n\nexport function buildInvitationUrl (groupId , inviteSecret ) {\n return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}`\n}\n\nexport const proposalSettingsType = objectOf({\n rule: ruleType,\n expires_ms: number,\n ruleSettings: objectOf({\n [RULE_PERCENTAGE]: objectOf({ threshold: number }),\n [RULE_DISAGREEMENT]: objectOf({ threshold: number })\n })\n})\n\n// returns true IF a single YES vote is required to pass the proposal\nexport function oneVoteToPass (proposalHash ) {\n const rootState = sbp('state/vuex/state')\n const state = rootState[rootState.currentGroupId]\n const proposal = state.proposals[proposalHash]\n const votes = Object.assign({}, proposal.votes)\n const currentResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n votes[String(Math.random())] = VOTE_FOR\n const newResult = rules[proposal.data.votingRule](state, proposal.data.proposalType, votes)\n console.debug(`oneVoteToPass currentResult(${currentResult}) newResult(${newResult})`)\n\n return currentResult === VOTE_UNDECIDED && newResult === VOTE_FOR\n}\n\nfunction voteAgainst (state , { meta, data, contractID } ) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_FAILED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_AGAINST, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n}\n\n// NOTE: The code is ready to receive different proposals settings,\n// However, we decided to make all settings the same, to simplify the UI/UX proptotype.\nexport const proposalDefaults = {\n rule: RULE_PERCENTAGE,\n expires_ms: 14 * DAYS_MILLIS,\n ruleSettings: ({\n [RULE_PERCENTAGE]: { threshold: 0.66 },\n [RULE_DISAGREEMENT]: { threshold: 1 }\n } )\n}\n\nconst proposals = {\n [PROPOSAL_INVITE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.payload = data.passPayload\n proposal.status = STATUS_PASSED\n // NOTE: if invite/process requires more than just data+meta\n // this code will need to be updated...\n const message = { meta, data: data.passPayload, contractID }\n sbp('gi.contracts/group/invite/process', message, state)\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n // TODO: for now, generate the link and send it to the user's inbox\n // however, we cannot send GIMessages in any way from here\n // because that means each time someone synchronizes this contract\n // a new invite would be sent...\n // which means the voter who casts the deciding ballot needs to\n // include in this message the authorization for the new user to\n // join the group.\n // we could make it so that all users generate the same authorization\n // somehow...\n // however if it's an OP_KEY_* message ther can only be one of those!\n //\n // so, the deciding voter is the one that generates the passPayload data for the\n // link includes the OP_KEY_* message in their final vote authorizing\n // the user.\n // additionally, for our testing purposes, we check to see if the\n // currently logged in user is the deciding voter, and if so we\n // send a message to that user's inbox with the link. The user\n // clicks the link and then generates an invite accept or invite decline message.\n //\n // we no longer have a state.invitees, instead we have a state.invites\n // sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n // TODO: generate and save invite link, which is used to generate a group/inviteAccept message\n // the invite link contains the secret to a public/private keypair that is generated\n // by the final voter, this keypair is a special \"write only\" keypair that is allowed\n // to only send a single kind of message to the group (accepting the invite, deleting\n // the writeonly keypair, and registering a new keypair with the group that has\n // full member privileges, i.e. their identity contract). The writeonly keypair\n // that's registered originally also contains attributes that tell the server\n // what kind of message(s) it's allowed to send to the contract, and how many\n // of them.\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_REMOVE_MEMBER]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash, passPayload } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n proposal.payload = passPayload\n const messageData = {\n ...proposal.data.proposalData,\n proposalHash,\n proposalPayload: passPayload\n }\n const message = { data: messageData, meta, contractID }\n sbp('gi.contracts/group/removeMember/process', message, state)\n sbp('gi.contracts/group/pushSideEffect', contractID,\n ['gi.contracts/group/removeMember/sideEffect', message]\n )\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GROUP_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const { setting, proposedValue } = proposal.data.proposalData\n // NOTE: if updateSettings ever needs more ethana just meta+data\n // this code will need to be updated\n const message = {\n meta,\n data: { [setting]: proposedValue },\n contractID\n }\n sbp('gi.contracts/group/updateSettings/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_PROPOSAL_SETTING_CHANGE]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n const message = {\n meta,\n data: proposal.data.proposalData,\n contractID\n }\n sbp('gi.contracts/group/updateAllVotingRules/process', message, state)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n },\n [PROPOSAL_GENERIC]: {\n defaults: proposalDefaults,\n [VOTE_FOR]: function (state, { meta, data, contractID }) {\n const { proposalHash } = data\n const proposal = state.proposals[proposalHash]\n proposal.status = STATUS_PASSED\n sbp('okTurtles.events/emit', PROPOSAL_RESULT, state, VOTE_FOR, data)\n archiveProposal({ state, proposalHash, proposal, contractID })\n },\n [VOTE_AGAINST]: voteAgainst\n }\n}\n\nexport default proposals\n\nexport const proposalType = unionOf(...Object.keys(proposals).map(k => literalOf(k)))\n"], + "mappings": ";;;AAKA,IAAM,YAAY,CAAC;AACnB,IAAM,UAAU,CAAC;AACjB,IAAM,gBAAgB,CAAC;AACvB,IAAM,gBAAgB,CAAC;AACvB,IAAM,kBAAkB,CAAC;AACzB,IAAM,kBAAkB,CAAC;AAEzB,IAAM,eAAe;AAErB,aAAc,aAAa,MAAM;AAC/B,QAAM,SAAS,mBAAmB,QAAQ;AAC1C,MAAI,CAAC,UAAU,WAAW;AACxB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AAGA,aAAW,WAAW,CAAC,gBAAgB,WAAW,cAAc,SAAS,aAAa,GAAG;AACvF,QAAI,SAAS;AACX,iBAAW,UAAU,SAAS;AAC5B,YAAI,OAAO,QAAQ,UAAU,IAAI,MAAM;AAAO;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACA,SAAO,UAAU,UAAU,KAAK,QAAQ,QAAQ,OAAO,GAAG,IAAI;AAChE;AAEO,4BAA6B,UAAU;AAC5C,QAAM,eAAe,aAAa,KAAK,QAAQ;AAC/C,MAAI,iBAAiB,MAAM;AACzB,UAAM,IAAI,MAAM,iCAAiC,UAAU;AAAA,EAC7D;AACA,SAAO,aAAa;AACtB;AAEA,IAAM,qBAAqB;AAAA,EACzB,0BAA0B,SAAU,MAAM;AACxC,UAAM,aAAa,CAAC;AACpB,eAAW,YAAY,MAAM;AAC3B,YAAM,SAAS,mBAAmB,QAAQ;AAC1C,UAAI,UAAU,WAAW;AACvB,QAAC,SAAQ,QAAQ,QAAQ,KAAK,6DAA6D,WAAW;AAAA,MACxG,WAAW,OAAO,KAAK,cAAc,YAAY;AAC/C,YAAI,gBAAgB,WAAW;AAE7B,UAAC,SAAQ,QAAQ,QAAQ,KAAK,6CAA6C,gDAAgD;AAAA,QAC7H;AACA,cAAM,KAAK,UAAU,YAAY,KAAK;AACtC,mBAAW,KAAK,QAAQ;AAExB,YAAI,CAAC,QAAQ,SAAS;AACpB,kBAAQ,UAAU,EAAE,OAAO,CAAC,EAAE;AAAA,QAChC;AAEA,YAAI,aAAa,GAAG,gBAAgB;AAClC,aAAG,KAAK,QAAQ,QAAQ,KAAK;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,4BAA4B,SAAU,MAAM;AAC1C,eAAW,YAAY,MAAM;AAC3B,UAAI,CAAC,gBAAgB,WAAW;AAC9B,cAAM,IAAI,MAAM,0CAA0C,UAAU;AAAA,MACtE;AACA,aAAO,UAAU;AAAA,IACnB;AAAA,EACF;AAAA,EACA,2BAA2B,SAAU,MAAM;AACzC,QAAI,4BAA4B,OAAO,KAAK,IAAI,CAAC;AACjD,WAAO,IAAI,0BAA0B,IAAI;AAAA,EAC3C;AAAA,EACA,wBAAwB,SAAU,MAAM;AACtC,eAAW,YAAY,MAAM;AAC3B,UAAI,UAAU,WAAW;AACvB,cAAM,IAAI,MAAM,mDAAmD;AAAA,MACrE;AACA,sBAAgB,YAAY;AAAA,IAC9B;AAAA,EACF;AAAA,EACA,sBAAsB,SAAU,MAAM;AACpC,eAAW,YAAY,MAAM;AAC3B,aAAO,gBAAgB;AAAA,IACzB;AAAA,EACF;AAAA,EACA,oBAAoB,SAAU,KAAK;AACjC,WAAO,UAAU;AAAA,EACnB;AAAA,EACA,0BAA0B,SAAU,QAAQ;AAC1C,kBAAc,KAAK,MAAM;AAAA,EAC3B;AAAA,EACA,0BAA0B,SAAU,QAAQ,QAAQ;AAClD,QAAI,CAAC,cAAc;AAAS,oBAAc,UAAU,CAAC;AACrD,kBAAc,QAAQ,KAAK,MAAM;AAAA,EACnC;AAAA,EACA,4BAA4B,SAAU,UAAU,QAAQ;AACtD,QAAI,CAAC,gBAAgB;AAAW,sBAAgB,YAAY,CAAC;AAC7D,oBAAgB,UAAU,KAAK,MAAM;AAAA,EACvC;AACF;AAEA,mBAAmB,0BAA0B,kBAAkB;AAE/D,IAAO,iBAAQ;;;ACpFf;;;ACNA;AACA;;;ACwBO,mBAAoB,KAA8B;AACvD,SAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;AACvC;;;ADtBO,IAAM,gBAAgB;AAAA,EAC3B,cAAc,CAAC,OAAO;AAAA,EACtB,cAAc,CAAC,KAAK,MAAM,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EAEtF,qBAAqB;AACvB;AAEA,IAAM,YAAY,CAAC,IAAI,YAAY;AACjC,MAAI,QAAQ,aAAa,QAAQ,OAAO;AACtC,QAAI,SAAS;AACb,QAAI,QAAQ,QAAQ,KAAK;AACvB,eAAS,UAAU,MAAM;AACzB,aAAO,aAAa,KAAK,QAAQ,QAAQ;AACzC,aAAO,aAAa,KAAK,GAAG;AAAA,IAC9B;AACA,OAAG,cAAc;AACjB,OAAG,YAAY,UAAU,SAAS,QAAQ,OAAO,MAAM,CAAC;AAAA,EAC1D;AACF;AAWA,IAAI,UAAU,aAAa;AAAA,EACzB,MAAM;AAAA,EACN,QAAQ;AACV,CAAC;;;AElDD;AACA;;;ACNA,IAAM,QAAQ;AAEC,kBAAmB,WAAmB,MAAqB;AACxE,QAAM,WAAW,KAAK;AAGtB,QAAM,oBACH,OAAO,aAAa,YAAY,aAAa,OAC1C,WACA;AAGN,SAAO,OAAO,QAAQ,OAAO,oBAAqB,OAAO,SAAS,OAAO;AAEvE,QAAI,OAAO,QAAQ,OAAO,OAAO,OAAO,QAAQ,MAAM,YAAY,KAAK;AACrE,aAAO;AAAA,IACT;AAEA,UAAM,mBAGJ,OAAO,UAAU,eAAe,KAAK,mBAAmB,OAAO,IAC3D,kBAAkB,WAClB;AAGN,QAAI,qBAAqB,QAAQ,qBAAqB,QAAW;AAC/D,aAAO;AAAA,IACT;AACA,WAAO,OAAO,gBAAgB;AAAA,EAChC,CAAC;AACH;;;ADtBA,KAAI,UAAU,IAAI;AAClB,KAAI,UAAU,QAAQ;AAEtB,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,0BAAgD,CAAC;AAOvD,IAAM,kBAAkB;AAAA,EACtB,GAAG;AAAA,EACH,cAAc,CAAC,SAAS,QAAQ,OAAO,QAAQ;AAAA,EAC/C,cAAc,CAAC,KAAK,KAAK,MAAM,UAAU,MAAM,KAAK,KAAK,SAAS,QAAQ,UAAU,OAAO,OAAO,GAAG;AAAA,EACrG,qBAAqB;AACvB;AAEA,IAAI,kBAAkB;AACtB,IAAI,sBAAsB,gBAAgB,MAAM,GAAG,EAAE;AACrD,IAAI,0BAA0B;AAY9B,eAAI,0BAA0B;AAAA,EAC5B,qBAAqB,oBAAqB,UAAiC;AAEzE,UAAM,CAAC,gBAAgB,SAAS,YAAY,EAAE,MAAM,GAAG;AAGvD,QAAI,SAAS,YAAY,MAAM,gBAAgB,YAAY;AAAG;AAI9D,QAAI,iBAAiB;AAAqB;AAG1C,QAAI,iBAAiB,qBAAqB;AACxC,wBAAkB;AAClB,4BAAsB;AACtB,gCAA0B;AAC1B;AAAA,IACF;AACA,QAAI;AACF,gCAA2B,MAAM,eAAI,4BAA4B,QAAQ,KAAM;AAG/E,wBAAkB;AAClB,4BAAsB;AAAA,IACxB,SAAS,OAAP;AACA,cAAQ,MAAM,KAAK;AAAA,IACrB;AAAA,EACF;AACF,CAAC;AA0CM,kBAAmB,MAAiC;AACzD,QAAM,IAAI;AAAA,IACR,OAAO;AAAA,EACT;AACA,aAAW,OAAO,MAAM;AACtB,MAAE,GAAG,UAAU,IAAI;AACnB,MAAE,IAAI,SAAS,KAAK;AAAA,EACtB;AACA,SAAO;AACT;AAEe,WACb,KACA,MACQ;AACR,SAAO,SAAS,wBAAwB,QAAQ,KAAK,IAAI,EAEtD,QAAQ,iBAAiB,QAAQ;AACtC;AAgBA,kBAAmB,aAAa;AAC9B,SAAO,WAAU,SAAS,aAAa,eAAe;AACxD;AAEA,KAAI,UAAU,QAAQ;AAAA,EACpB,YAAY;AAAA,EACZ,OAAO;AAAA,IACL,MAAM,CAAC,QAAQ,KAAK;AAAA,IACpB,KAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA,SAAS;AAAA,EACX;AAAA,EACA,QAAQ,SAAU,GAAG,SAAS;AAC5B,UAAM,OAAO,QAAQ,SAAS,GAAG;AACjC,UAAM,cAAc,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,CAAC;AACpD,QAAI,CAAC,aAAa;AAChB,cAAQ,KAAK,yDAAyD,IAAI;AAC1E,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,IAAI;AAAA,IAChD;AAEA,QAAI,QAAQ,MAAM,QAAQ,OAAO,QAAQ,KAAK,MAAM,WAAW,UAAU;AACvE,cAAQ,KAAK,MAAM,MAAM;AAAA,IAC3B;AACA,QAAI,QAAQ,MAAM,SAAS;AACzB,YAAM,SAAS,KAAI,QAAQ,WAAW,SAAS,WAAW,IAAI,SAAS;AAEvE,aAAO,OAAO,OAAO,KAAK;AAAA,QACxB,IAAI,CAAC,QAAQ,SAAS;AACpB,cAAI,QAAQ,QAAQ;AAClB,mBAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,GAAG,IAAI;AAAA,UACnD,OAAO;AACL,mBAAO,EAAE,KAAK,GAAG,IAAI;AAAA,UACvB;AAAA,QACF;AAAA,QACA,IAAI,OAAK;AAAA,MACX,CAAC;AAAA,IACH,OAAO;AACL,UAAI,CAAC,QAAQ,KAAK;AAAU,gBAAQ,KAAK,WAAW,CAAC;AACrD,cAAQ,KAAK,SAAS,YAAY,SAAS,WAAW;AACtD,aAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,IAAI;AAAA,IAC1C;AAAA,EACF;AACF,CAAC;;;AE5IM,IAAM,cAAc,OAAO,SAAS;AACpC,IAAM,UAAU,OAAK,MAAM;AAC3B,IAAM,QAAQ,OAAK,MAAM;AACzB,IAAM,UAAU,OAAK,OAAO,MAAM;AAClC,IAAM,YAAY,OAAK,OAAO,MAAM;AACpC,IAAM,WAAW,OAAK,OAAO,MAAM;AAEnC,IAAM,WAAW,OAAK,CAAC,MAAM,CAAC,KAAK,OAAO,MAAM;AAChD,IAAM,aAAa,OAAK,OAAO,MAAM;AAcrC,IAAM,UAAU,CAAC,QAAQ,aAAa;AAC3C,MAAI,WAAW,OAAO,IAAI;AAAG,WAAO,OAAO,KAAK,QAAQ;AACxD,SAAO,OAAO,QAAQ;AACxB;AAGO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YACE,SACA,cACA,WACA,OACA,WAAmB,IACnB,YAAqB,IACrB;AACA,UAAM,aAAa,WACjB,YAAY,0BAA0B,YAAY;AACpD,UAAM,UAAU;AAChB,SAAK,eAAe;AACpB,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,YAAY,aAAa;AAC9B,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,UAAU,GAAG;AAAA,EAAe,KAAK,aAAa;AACnD,SAAK,OAAO,KAAK,YAAY;AAC7B,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,kBAAkB;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,gBAAyB;AACvB,UAAM,YAAY,KAAK,MAAM,MAAM,gCAAgC,KAAK,CAAC;AACzE,WAAO,UAAU,KAAK,cAAY,SAAS,QAAQ,qBAAqB,MAAM,EAAE,KAAK;AAAA,EACvF;AAAA,EAEA,eAAwB;AACtB,WAAO;AAAA,eACI,KAAK;AAAA,eACL,KAAK;AAAA,eACL,KAAK,aAAa,QAAQ,OAAO,EAAE;AAAA,eACnC,KAAK;AAAA,eACL,KAAK;AAAA;AAAA,EAElB;AACF;AAKA,IAAM,iBAAoB,CACxB,QACA,OACA,OACA,SACA,cACA,cACuB;AACvB,SAAO,IAAI,mBACT,SACA,gBAAgB,QAAQ,MAAM,GAC9B,aAAa,OAAO,OACpB,KAAK,UAAU,KAAK,GACpB,OAAO,MACP,KACF;AACF;AAgBO,IAAM,YACM,CAAC,cAAmC;AACnD,mBAAkB,OAAO,SAAS,IAAI;AACpC,QAAI,QAAQ,KAAK,KAAM,UAAU;AAAY,aAAO;AACpD,UAAM,eAAe,SAAS,OAAO,MAAM;AAAA,EAC7C;AACA,UAAQ,OAAO,MAAM;AACnB,QAAI,UAAU,SAAS;AAAG,aAAO,GAAG,YAAY,SAAS;AAAA;AACpD,aAAO,IAAI;AAAA,EAClB;AACA,SAAO;AACT;AAyCK,IAAM,SACX,SAAU,OAAO;AACf,MAAI,QAAQ,KAAK;AAAG,WAAO,CAAC;AAC5B,MAAI,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,KAAK,GAAG;AAC5C,WAAO,OAAO,OAAO,CAAC,GAAG,KAAK;AAAA,EAChC;AACA,QAAM,eAAe,QAAQ,KAAK;AACpC;AAGK,IAAM,WACX,CAAC,SAAY,SAAkB,aAAoE;AACnG,mBAAkB,OAAO;AACvB,UAAM,IAAI,OAAO,KAAK;AACtB,UAAM,YAAY,OAAO,KAAK,OAAO;AACrC,UAAM,cAAc,OAAO,KAAK,CAAC,EAAE,KAAK,UAAQ,CAAC,UAAU,SAAS,IAAI,CAAC;AACzE,QAAI,aAAa;AACf,YAAM,eACJ,SACA,OACA,QACA,4BAA4B,mBAAmB,aACjD;AAAA,IACF;AAIA,UAAM,YAAY,UAAU,KAAK,cAAY;AAC3C,YAAM,iBAAiB,QAAQ;AAC/B,aAAQ,eAAe,KAAK,SAAS,OAAO,KAAK,CAAC,EAAE,eAAe,QAAQ;AAAA,IAC7E,CAAC;AACD,QAAI,WAAW;AACb,YAAM,eACJ,SACA,EAAE,YACF,GAAG,UAAU,aACb,0BAA0B,kBAAkB,eAC5C,iBAAiB,QAAQ,QAAQ,UAAU,EAAE,OAAO,CAAC,KACrD,GACF;AAAA,IACF;AAEA,UAAM,UAAU,QAAQ,KAAK,IACzB,CAAC,KAAK,QAAQ,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,CAAC,IAC/D,CAAC,KAAK,QAAQ;AACd,YAAM,SAAS,QAAQ;AACvB,UAAI,OAAO,KAAK,SAAS,UAAU,KAAK,CAAC,EAAE,eAAe,GAAG,GAAG;AAC9D,eAAO,OAAO,OAAO,KAAK,CAAC,CAAC;AAAA,MAC9B,OAAO;AACL,eAAO,OAAO,OAAO,KAAK,EAAE,CAAC,MAAM,OAAO,EAAE,MAAM,GAAG,UAAU,KAAK,EAAE,CAAC;AAAA,MACzE;AAAA,IACF;AACF,WAAO,UAAU,OAAO,SAAS,CAAC,CAAC;AAAA,EACrC;AACA,UAAQ,OAAO,MAAM;AACnB,UAAM,QAAQ,OAAO,KAAK,OAAO,EAAE,IACjC,CAAC,QAAQ;AACP,YAAM,MAAM,QAAQ,KAAK,KAAK,SAAS,UAAU,IAC/C,GAAG,SAAS,QAAQ,QAAQ,MAAM,EAAE,QAAQ,KAAK,CAAC,MAClD,GAAG,QAAQ,QAAQ,QAAQ,IAAI;AACjC,aAAO;AAAA,IACT,CACF;AACA,WAAO;AAAA,GAAQ,MAAM,KAAK,OAAO;AAAA;AAAA,EACnC;AACA,SAAO;AACT;AA+BO,eAAgB,OAAO,SAAS,IAAI;AACzC,MAAI,QAAQ,KAAK,KAAK,QAAQ,KAAK;AAAG,WAAO;AAC7C,QAAM,eAAe,OAAO,OAAO,MAAM;AAC3C;AACA,MAAM,OAAO,MAAM;AAYZ,IAAM,SACX,iBAAiB,OAAO,SAAS,IAAI;AACnC,MAAI,QAAQ,KAAK;AAAG,WAAO;AAC3B,MAAI,SAAS,KAAK;AAAG,WAAO;AAC5B,QAAM,eAAe,SAAQ,OAAO,MAAM;AAC5C;AA2DF,qBAAsB,WAAW;AAC/B,iBAAgB,OAAc,SAAS,IAAI;AACzC,eAAW,UAAU,WAAW;AAC9B,UAAI;AACF,eAAO,OAAO,OAAO,MAAM;AAAA,MAC7B,SAAS,GAAP;AAAA,MAAW;AAAA,IACf;AACA,UAAM,eAAe,OAAO,OAAO,MAAM;AAAA,EAC3C;AACA,QAAM,OAAO,MAAM,IAAI,UAAU,IAAI,QAAM,QAAQ,EAAE,CAAC,EAAE,KAAK,KAAK;AAClE,SAAO;AACT;AAGO,IAAM,UAAU;;;AC/YhB,IAAM,cAAc;AACpB,IAAM,eAAe,KAAK;AAC1B,IAAM,cAAc,KAAK;AACzB,IAAM,gBAAgB,KAAK;;;ACQ3B,IAAM,iBAAiB;AAAA,EAC5B,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AACX;AAEO,IAAM,kBAAkB;AACxB,IAAM,yBAAyB;AAC/B,IAAM,yBAAyB;AAC/B,IAAM,gCAAgC;AACtC,IAAM,mCAAmC;AACzC,IAAM,mBAAmB;AAMzB,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;;;ACzBtB,IAAM,eAAe;AACrB,IAAM,mBAAmB;AACzB,IAAM,iBAAiB;AACvB,IAAM,WAAW;AAEjB,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAC1B,IAAM,oBAAoB;AAejC,IAAM,gBAAgB,CAAC,UAAU,OAAO,KAAK,MAAM,QAAQ,EAAE,OAAO,OAAK,MAAM,SAAS,GAAG,WAAW,eAAe,MAAM,EAAE;AAE7H,IAAM,QAAgB;AAAA,EACpB,CAAC,kBAAkB,SAAU,OAAO,eAAc,OAAO;AACvD,YAAQ,OAAO,OAAO,KAAK;AAC3B,QAAI,aAAa,cAAc,KAAK;AACpC,QAAI,kBAAiB;AAAwB,oBAAc;AAC3D,UAAM,mBAAmB,MAAM,SAAS,UAAU,eAAc,aAAa,iBAAiB;AAC9F,UAAM,YAAY,qBAAqB,iBAAiB,kBAAkB,UAAU;AACpF,UAAM,mBAAmB,MAAM,OAAO,OAAK,MAAM,gBAAgB,EAAE;AACnE,UAAM,WAAW,MAAM,OAAO,OAAK,MAAM,QAAQ,EAAE;AACnD,UAAM,eAAe,MAAM,OAAO,OAAK,MAAM,YAAY,EAAE;AAC3D,UAAM,oBAAoB,WAAW;AACrC,UAAM,UAAU,oBAAoB;AACpC,UAAM,SAAS,aAAa;AAK5B,UAAM,eAAe,KAAK,KAAK,YAAa,cAAa,iBAAiB;AAC1E,YAAQ,MAAM,cAAc,uBAAuB,kBAAiB,EAAE,cAAc,UAAU,cAAc,kBAAkB,WAAW,QAAQ,SAAS,WAAW,CAAC;AACtK,QAAI,YAAY,cAAc;AAC5B,aAAO;AAAA,IACT;AACA,WAAO,WAAW,SAAS,eAAe,eAAe;AAAA,EAC3D;AAAA,EACA,CAAC,oBAAoB,SAAU,OAAO,eAAc,OAAO;AACzD,YAAQ,OAAO,OAAO,KAAK;AAC3B,UAAM,aAAa,cAAc,KAAK;AACtC,UAAM,aAAa,kBAAiB,yBAAyB,IAAI;AACjE,UAAM,oBAAoB,KAAK,IAAI,MAAM,SAAS,UAAU,eAAc,aAAa,mBAAmB,WAAW,UAAU;AAC/H,UAAM,YAAY,qBAAqB,mBAAmB,mBAAmB,UAAU;AACvF,UAAM,WAAW,MAAM,OAAO,OAAK,MAAM,QAAQ,EAAE;AACnD,UAAM,eAAe,MAAM,OAAO,OAAK,MAAM,YAAY,EAAE;AAC3D,UAAM,UAAU,MAAM;AACtB,UAAM,SAAS,aAAa;AAE5B,YAAQ,MAAM,cAAc,yBAAyB,kBAAiB,EAAE,UAAU,cAAc,WAAW,SAAS,YAAY,OAAO,CAAC;AACxI,QAAI,gBAAgB,WAAW;AAC7B,aAAO;AAAA,IACT;AAGA,WAAO,eAAe,SAAS,YAAY,WAAW;AAAA,EACxD;AAAA,EACA,CAAC,oBAAoB,SAAU,OAAO,eAAc,OAAO;AACzD,UAAM,IAAI,MAAM,gBAAgB;AAAA,EAMlC;AACF;AAEA,IAAO,gBAAQ;AAER,IAAM,WAAgB,QAAQ,GAAG,OAAO,KAAK,KAAK,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;AAc1E,IAAM,uBAAuB,CAAC,MAAc,WAAmB,cAA8B;AAClG,QAAM,kBAAkB,KAAK,IAAI,GAAG,SAAS;AAE7C,SAAO;AAAA,IACL,CAAC,oBAAoB,MAAM;AAEzB,aAAO,KAAK,IAAI,kBAAkB,GAAG,SAAS;AAAA,IAChD;AAAA,IACA,CAAC,kBAAkB,MAAM;AAEvB,YAAM,eAAe,IAAI;AACzB,aAAO,KAAK,IAAI,cAAc,SAAS;AAAA,IACzC;AAAA,EACF,EAAE,MAAM;AACV;;;AC9FO,yBACL,EAAE,OAAO,cAAc,UAAU,cAE9B;AACH,WAAI,OAAO,MAAM,WAAW,YAAY;AACxC,iBAAI,qCAAqC,YACvC,CAAC,sCAAsC,YAAY,cAAc,QAAQ,CAC3E;AACF;AAEO,4BAA6B,SAAiB,cAA8B;AACjF,SAAO,GAAG,SAAS,2BAA2B,kBAAkB;AAClE;AAEO,IAAM,uBAA4B,SAAS;AAAA,EAChD,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,cAAc,SAAS;AAAA,IACrB,CAAC,kBAAkB,SAAS,EAAE,WAAW,OAAO,CAAC;AAAA,IACjD,CAAC,oBAAoB,SAAS,EAAE,WAAW,OAAO,CAAC;AAAA,EACrD,CAAC;AACH,CAAC;AAGM,uBAAwB,cAA+B;AAC5D,QAAM,YAAY,eAAI,kBAAkB;AACxC,QAAM,QAAQ,UAAU,UAAU;AAClC,QAAM,WAAW,MAAM,UAAU;AACjC,QAAM,QAAQ,OAAO,OAAO,CAAC,GAAG,SAAS,KAAK;AAC9C,QAAM,gBAAgB,cAAM,SAAS,KAAK,YAAY,OAAO,SAAS,KAAK,cAAc,KAAK;AAC9F,QAAM,OAAO,KAAK,OAAO,CAAC,KAAK;AAC/B,QAAM,YAAY,cAAM,SAAS,KAAK,YAAY,OAAO,SAAS,KAAK,cAAc,KAAK;AAC1F,UAAQ,MAAM,+BAA+B,4BAA4B,YAAY;AAErF,SAAO,kBAAkB,kBAAkB,cAAc;AAC3D;AAEA,qBAAsB,OAAY,EAAE,MAAM,MAAM,cAAmB;AACjE,QAAM,EAAE,iBAAiB;AACzB,QAAM,WAAW,MAAM,UAAU;AACjC,WAAS,SAAS;AAClB,iBAAI,yBAAyB,iBAAiB,OAAO,cAAc,IAAI;AACvE,kBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAC/D;AAIO,IAAM,mBAAmB;AAAA,EAC9B,MAAM;AAAA,EACN,YAAY,KAAK;AAAA,EACjB,cAAe;AAAA,IACb,CAAC,kBAAkB,EAAE,WAAW,KAAK;AAAA,IACrC,CAAC,oBAAoB,EAAE,WAAW,EAAE;AAAA,EACtC;AACF;AAEA,IAAM,YAAoB;AAAA,EACxB,CAAC,yBAAyB;AAAA,IACxB,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,UAAU,KAAK;AACxB,eAAS,SAAS;AAGlB,YAAM,UAAU,EAAE,MAAM,MAAM,KAAK,aAAa,WAAW;AAC3D,qBAAI,qCAAqC,SAAS,KAAK;AACvD,qBAAI,yBAAyB,iBAAiB,OAAO,UAAU,IAAI;AACnE,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IA+B/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,yBAAyB;AAAA,IACxB,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,cAAc,gBAAgB;AACtC,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,eAAS,UAAU;AACnB,YAAM,cAAc;AAAA,QAClB,GAAG,SAAS,KAAK;AAAA,QACjB;AAAA,QACA,iBAAiB;AAAA,MACnB;AACA,YAAM,UAAU,EAAE,MAAM,aAAa,MAAM,WAAW;AACtD,qBAAI,2CAA2C,SAAS,KAAK;AAC7D,qBAAI,qCAAqC,YACvC,CAAC,8CAA8C,OAAO,CACxD;AACA,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,gCAAgC;AAAA,IAC/B,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,YAAM,EAAE,SAAS,kBAAkB,SAAS,KAAK;AAGjD,YAAM,UAAU;AAAA,QACd;AAAA,QACA,MAAM,EAAE,CAAC,UAAU,cAAc;AAAA,QACjC;AAAA,MACF;AACA,qBAAI,6CAA6C,SAAS,KAAK;AAC/D,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,mCAAmC;AAAA,IAClC,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,YAAM,UAAU;AAAA,QACd;AAAA,QACA,MAAM,SAAS,KAAK;AAAA,QACpB;AAAA,MACF;AACA,qBAAI,mDAAmD,SAAS,KAAK;AACrE,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAAA,EACA,CAAC,mBAAmB;AAAA,IAClB,UAAU;AAAA,IACV,CAAC,WAAW,SAAU,OAAO,EAAE,MAAM,MAAM,cAAc;AACvD,YAAM,EAAE,iBAAiB;AACzB,YAAM,WAAW,MAAM,UAAU;AACjC,eAAS,SAAS;AAClB,qBAAI,yBAAyB,iBAAiB,OAAO,UAAU,IAAI;AACnE,sBAAgB,EAAE,OAAO,cAAc,UAAU,WAAW,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AACF;AAEA,IAAO,oBAAQ;AAER,IAAM,eAAoB,QAAQ,GAAG,OAAO,KAAK,SAAS,EAAE,IAAI,OAAK,UAAU,CAAC,CAAC,CAAC;", "names": [] } From f1d87f1d9d9675fd3b352d5b5513f154b78f2c51 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Thu, 20 Oct 2022 13:05:00 +0200 Subject: [PATCH 30/43] Bump contracts version to 0.0.8 --- contracts/0.0.8/chatroom-slim.js | 795 ++ contracts/0.0.8/chatroom.0.0.8.manifest.json | 1 + contracts/0.0.8/chatroom.js | 9897 +++++++++++++++ contracts/0.0.8/group-slim.js | 1680 +++ contracts/0.0.8/group.0.0.8.manifest.json | 1 + contracts/0.0.8/group.js | 10810 +++++++++++++++++ contracts/0.0.8/identity-slim.js | 288 + contracts/0.0.8/identity.0.0.8.manifest.json | 1 + contracts/0.0.8/identity.js | 9391 ++++++++++++++ contracts/0.0.8/mailbox-slim.js | 325 + contracts/0.0.8/mailbox.0.0.8.manifest.json | 1 + contracts/0.0.8/mailbox.js | 9433 ++++++++++++++ frontend/model/contracts/manifests.json | 8 +- package.json | 2 +- 14 files changed, 42628 insertions(+), 5 deletions(-) create mode 100644 contracts/0.0.8/chatroom-slim.js create mode 100644 contracts/0.0.8/chatroom.0.0.8.manifest.json create mode 100644 contracts/0.0.8/chatroom.js create mode 100644 contracts/0.0.8/group-slim.js create mode 100644 contracts/0.0.8/group.0.0.8.manifest.json create mode 100644 contracts/0.0.8/group.js create mode 100644 contracts/0.0.8/identity-slim.js create mode 100644 contracts/0.0.8/identity.0.0.8.manifest.json create mode 100644 contracts/0.0.8/identity.js create mode 100644 contracts/0.0.8/mailbox-slim.js create mode 100644 contracts/0.0.8/mailbox.0.0.8.manifest.json create mode 100644 contracts/0.0.8/mailbox.js diff --git a/contracts/0.0.8/chatroom-slim.js b/contracts/0.0.8/chatroom-slim.js new file mode 100644 index 0000000000..a6ab179b7b --- /dev/null +++ b/contracts/0.0.8/chatroom-slim.js @@ -0,0 +1,795 @@ +"use strict"; +(() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { + get: (a, b) => (typeof require !== "undefined" ? require : a)[b] + }) : x)(function(x) { + if (typeof require !== "undefined") + return require.apply(this, arguments); + throw new Error('Dynamic require of "' + x + '" is not supported'); + }); + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); + + // frontend/model/contracts/chatroom.js + var import_sbp3 = __toESM(__require("@sbp/sbp")); + var import_common2 = __require("@common/common.js"); + + // frontend/model/contracts/shared/giLodash.js + function cloneDeep(obj) { + return JSON.parse(JSON.stringify(obj)); + } + function isMergeableObject(val) { + const nonNullObject = val && typeof val === "object"; + return nonNullObject && Object.prototype.toString.call(val) !== "[object RegExp]" && Object.prototype.toString.call(val) !== "[object Date]"; + } + function merge(obj, src) { + for (const key in src) { + const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : void 0; + if (clone && isMergeableObject(obj[key])) { + merge(obj[key], clone); + continue; + } + obj[key] = clone || src[key]; + } + return obj; + } + + // frontend/model/contracts/shared/constants.js + var CHATROOM_NAME_LIMITS_IN_CHARS = 50; + var CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280; + var CHATROOM_ACTIONS_PER_PAGE = 40; + var CHATROOM_MESSAGES_PER_PAGE = 20; + var CHATROOM_MESSAGE_ACTION = "chatroom-message-action"; + var MESSAGE_RECEIVE = "message-receive"; + var CHATROOM_TYPES = { + INDIVIDUAL: "individual", + GROUP: "group" + }; + var CHATROOM_PRIVACY_LEVEL = { + GROUP: "chatroom-privacy-level-group", + PRIVATE: "chatroom-privacy-level-private", + PUBLIC: "chatroom-privacy-level-public" + }; + var MESSAGE_TYPES = { + POLL: "message-poll", + TEXT: "message-text", + INTERACTIVE: "message-interactive", + NOTIFICATION: "message-notification" + }; + var MESSAGE_NOTIFICATIONS = { + ADD_MEMBER: "add-member", + JOIN_MEMBER: "join-member", + LEAVE_MEMBER: "leave-member", + KICK_MEMBER: "kick-member", + UPDATE_DESCRIPTION: "update-description", + UPDATE_NAME: "update-name", + DELETE_CHANNEL: "delete-channel", + VOTE: "vote" + }; + var MAIL_TYPE_MESSAGE = "message"; + var MAIL_TYPE_FRIEND_REQ = "friend-request"; + + // frontend/model/contracts/misc/flowTyper.js + var EMPTY_VALUE = Symbol("@@empty"); + var isEmpty = (v) => v === EMPTY_VALUE; + var isNil = (v) => v === null; + var isUndef = (v) => typeof v === "undefined"; + var isBoolean = (v) => typeof v === "boolean"; + var isNumber = (v) => typeof v === "number"; + var isString = (v) => typeof v === "string"; + var isObject = (v) => !isNil(v) && typeof v === "object"; + var isFunction = (v) => typeof v === "function"; + var getType = (typeFn, _options) => { + if (isFunction(typeFn.type)) + return typeFn.type(_options); + return typeFn.name || "?"; + }; + var TypeValidatorError = class extends Error { + expectedType; + valueType; + value; + typeScope; + sourceFile; + constructor(message, expectedType, valueType, value, typeName = "", typeScope = "") { + const errMessage = message || `invalid "${valueType}" value type; ${typeName || expectedType} type expected`; + super(errMessage); + this.expectedType = expectedType; + this.valueType = valueType; + this.value = value; + this.typeScope = typeScope || ""; + this.sourceFile = this.getSourceFile(); + this.message = `${errMessage} +${this.getErrorInfo()}`; + this.name = this.constructor.name; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, TypeValidatorError); + } + } + getSourceFile() { + const fileNames = this.stack.match(/(\/[\w_\-.]+)+(\.\w+:\d+:\d+)/g) || []; + return fileNames.find((fileName) => fileName.indexOf("/flowTyper-js/dist/") === -1) || ""; + } + getErrorInfo() { + return ` + file ${this.sourceFile} + scope ${this.typeScope} + expected ${this.expectedType.replace(/\n/g, "")} + type ${this.valueType} + value ${this.value} +`; + } + }; + var validatorError = (typeFn, value, scope, message, expectedType, valueType) => { + return new TypeValidatorError(message, expectedType || getType(typeFn), valueType || typeof value, JSON.stringify(value), typeFn.name, scope); + }; + var arrayOf = (typeFn, _scope = "Array") => { + function array(value) { + if (isEmpty(value)) + return [typeFn(value)]; + if (Array.isArray(value)) { + let index = 0; + return value.map((v) => typeFn(v, `${_scope}[${index++}]`)); + } + throw validatorError(array, value, _scope); + } + array.type = () => `Array<${getType(typeFn)}>`; + return array; + }; + var literalOf = (primitive) => { + function literal(value, _scope = "") { + if (isEmpty(value) || value === primitive) + return primitive; + throw validatorError(literal, value, _scope); + } + literal.type = () => { + if (isBoolean(primitive)) + return `${primitive ? "true" : "false"}`; + else + return `"${primitive}"`; + }; + return literal; + }; + var mapOf = (keyTypeFn, typeFn) => { + function mapOf2(value) { + if (isEmpty(value)) + return {}; + const o = object(value); + const reducer = (acc, key) => Object.assign(acc, { + [keyTypeFn(key, "Map[_]")]: typeFn(o[key], `Map.${key}`) + }); + return Object.keys(o).reduce(reducer, {}); + } + mapOf2.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`; + return mapOf2; + }; + var object = function(value) { + if (isEmpty(value)) + return {}; + if (isObject(value) && !Array.isArray(value)) { + return Object.assign({}, value); + } + throw validatorError(object, value); + }; + var objectOf = (typeObj, _scope = "Object") => { + function object2(value) { + const o = object(value); + const typeAttrs = Object.keys(typeObj); + const unknownAttr = Object.keys(o).find((attr) => !typeAttrs.includes(attr)); + if (unknownAttr) { + throw validatorError(object2, value, _scope, `missing object property '${unknownAttr}' in ${_scope} type`); + } + const undefAttr = typeAttrs.find((property) => { + const propertyTypeFn = typeObj[property]; + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); + }); + if (undefAttr) { + throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); + } + const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { + const typeFn = typeObj[key]; + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { + return Object.assign(acc, {}); + } else { + return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); + } + }; + return typeAttrs.reduce(reducer, {}); + } + object2.type = () => { + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); + return `{| + ${props.join(",\n ")} +|}`; + }; + return object2; + }; + function objectMaybeOf(validations, _scope = "Object") { + return function(data) { + object(data); + for (const key in data) { + validations[key]?.(data[key], `${_scope}.${key}`); + } + return data; + }; + } + var optional = (typeFn) => { + const unionFn = unionOf(typeFn, undef); + function optional2(v) { + return unionFn(v); + } + optional2.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn); + return optional2; + }; + function undef(value, _scope = "") { + if (isEmpty(value) || isUndef(value)) + return void 0; + throw validatorError(undef, value, _scope); + } + undef.type = () => "void"; + var number = function number2(value, _scope = "") { + if (isEmpty(value)) + return 0; + if (isNumber(value)) + return value; + throw validatorError(number2, value, _scope); + }; + var string = function string2(value, _scope = "") { + if (isEmpty(value)) + return ""; + if (isString(value)) + return value; + throw validatorError(string2, value, _scope); + }; + function unionOf_(...typeFuncs) { + function union(value, _scope = "") { + for (const typeFn of typeFuncs) { + try { + return typeFn(value, _scope); + } catch (_) { + } + } + throw validatorError(union, value, _scope); + } + union.type = () => `(${typeFuncs.map((fn) => getType(fn)).join(" | ")})`; + return union; + } + var unionOf = unionOf_; + + // frontend/model/contracts/shared/types.js + var inviteType = objectOf({ + inviteSecret: string, + quantity: number, + creator: string, + invitee: optional(string), + status: string, + responses: mapOf(string, string), + expires: number + }); + var chatRoomAttributesType = objectOf({ + name: string, + description: string, + type: unionOf(...Object.values(CHATROOM_TYPES).map((v) => literalOf(v))), + privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map((v) => literalOf(v))) + }); + var messageType = objectMaybeOf({ + type: unionOf(...Object.values(MESSAGE_TYPES).map((v) => literalOf(v))), + text: string, + notification: objectMaybeOf({ + type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map((v) => literalOf(v))), + params: mapOf(string, string) + }), + replyingMessage: objectOf({ + id: string, + text: string + }), + emoticons: mapOf(string, arrayOf(string)), + onlyVisibleTo: arrayOf(string) + }); + var mailType = unionOf(...[MAIL_TYPE_MESSAGE, MAIL_TYPE_FRIEND_REQ].map((k) => literalOf(k))); + + // frontend/model/contracts/shared/functions.js + var import_sbp = __toESM(__require("@sbp/sbp")); + + // frontend/model/contracts/shared/time.js + var import_common = __require("@common/common.js"); + var MINS_MILLIS = 6e4; + var HOURS_MILLIS = 60 * MINS_MILLIS; + var DAYS_MILLIS = 24 * HOURS_MILLIS; + var MONTHS_MILLIS = 30 * DAYS_MILLIS; + + // frontend/views/utils/misc.js + function logExceptNavigationDuplicated(err) { + err.name !== "NavigationDuplicated" && console.error(err); + } + + // frontend/model/contracts/shared/functions.js + function createMessage({ meta, data, hash, state }) { + const { type, text, replyingMessage } = data; + const { createdDate } = meta; + let newMessage = { + type, + datetime: new Date(createdDate).toISOString(), + id: hash, + from: meta.username + }; + if (type === MESSAGE_TYPES.TEXT) { + newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }; + } else if (type === MESSAGE_TYPES.POLL) { + } else if (type === MESSAGE_TYPES.NOTIFICATION) { + const params = { + channelName: state?.attributes.name, + channelDescription: state?.attributes.description, + ...data.notification + }; + delete params.type; + newMessage = { + ...newMessage, + notification: { type: data.notification.type, params } + }; + } else if (type === MESSAGE_TYPES.INTERACTIVE) { + } + return newMessage; + } + async function leaveChatRoom({ contractID }) { + const rootState = (0, import_sbp.default)("state/vuex/state"); + const rootGetters = (0, import_sbp.default)("state/vuex/getters"); + if (contractID === rootGetters.currentChatRoomId) { + (0, import_sbp.default)("state/vuex/commit", "setCurrentChatRoomId", { + groupId: rootState.currentGroupId + }); + const curRouteName = (0, import_sbp.default)("controller/router").history.current.name; + if (curRouteName === "GroupChat" || curRouteName === "GroupChatConversation") { + await (0, import_sbp.default)("controller/router").push({ name: "GroupChatConversation", params: { chatRoomId: rootGetters.currentChatRoomId } }).catch(logExceptNavigationDuplicated); + } + } + (0, import_sbp.default)("state/vuex/commit", "deleteChatRoomUnread", { chatRoomId: contractID }); + (0, import_sbp.default)("state/vuex/commit", "deleteChatRoomScrollPosition", { chatRoomId: contractID }); + (0, import_sbp.default)("chelonia/contract/remove", contractID).catch((e) => { + console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e); + }); + } + function findMessageIdx(id, messages) { + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].id === id) { + return i; + } + } + return -1; + } + function makeMentionFromUsername(username) { + return { + me: `@${username}`, + all: "@all" + }; + } + + // frontend/model/contracts/shared/nativeNotification.js + var import_sbp2 = __toESM(__require("@sbp/sbp")); + function makeNotification({ title, body, icon, path }) { + const notificationEnabled = (0, import_sbp2.default)("state/vuex/state").notificationEnabled; + if (typeof Notification === "undefined" || Notification.permission !== "granted" || !notificationEnabled) { + return; + } + const notification = new Notification(title, { body, icon }); + if (path) { + notification.onclick = function(event) { + event.preventDefault(); + (0, import_sbp2.default)("controller/router").push({ path }).catch(console.warn); + }; + } + } + + // frontend/model/contracts/chatroom.js + function createNotificationData(notificationType, moreParams = {}) { + return { + type: MESSAGE_TYPES.NOTIFICATION, + notification: { + type: notificationType, + ...moreParams + } + }; + } + function emitMessageEvent({ contractID, hash }) { + (0, import_sbp3.default)("okTurtles.events/emit", `${CHATROOM_MESSAGE_ACTION}-${contractID}`, { hash }); + } + function addMention({ contractID, messageId, datetime, text, username, chatRoomName }) { + if ((0, import_sbp3.default)("okTurtles.data/get", "READY_TO_JOIN_CHATROOM")) { + return; + } + (0, import_sbp3.default)("state/vuex/commit", "addChatRoomUnreadMention", { + chatRoomId: contractID, + messageId, + createdDate: datetime + }); + const rootGetters = (0, import_sbp3.default)("state/vuex/getters"); + const groupID = rootGetters.groupIdFromChatRoomId(contractID); + const path = `/group-chat/${contractID}`; + makeNotification({ + title: `# ${chatRoomName}`, + body: text, + icon: rootGetters.globalProfile2(groupID, username)?.picture, + path + }); + (0, import_sbp3.default)("okTurtles.events/emit", MESSAGE_RECEIVE); + } + function deleteMention({ contractID, messageId }) { + (0, import_sbp3.default)("state/vuex/commit", "deleteChatRoomUnreadMention", { chatRoomId: contractID, messageId }); + } + function updateUnreadPosition({ contractID, hash, createdDate }) { + (0, import_sbp3.default)("state/vuex/commit", "setChatRoomUnreadSince", { + chatRoomId: contractID, + messageId: hash, + createdDate + }); + } + (0, import_sbp3.default)("chelonia/defineContract", { + name: "gi.contracts/chatroom", + metadata: { + validate: objectOf({ + createdDate: string, + username: string, + identityContractID: string + }), + create() { + const { username, identityContractID } = (0, import_sbp3.default)("state/vuex/state").loggedIn; + return { + createdDate: new Date().toISOString(), + username, + identityContractID + }; + } + }, + getters: { + currentChatRoomState(state) { + return state; + }, + chatRoomSettings(state, getters) { + return getters.currentChatRoomState.settings || {}; + }, + chatRoomAttributes(state, getters) { + return getters.currentChatRoomState.attributes || {}; + }, + chatRoomUsers(state, getters) { + return getters.currentChatRoomState.users || {}; + }, + chatRoomLatestMessages(state, getters) { + return getters.currentChatRoomState.messages || []; + } + }, + actions: { + "gi.contracts/chatroom": { + validate: objectOf({ + attributes: chatRoomAttributesType + }), + process({ meta, data }, { state }) { + const initialState = merge({ + settings: { + actionsPerPage: CHATROOM_ACTIONS_PER_PAGE, + messagesPerPage: CHATROOM_MESSAGES_PER_PAGE, + maxNameLength: CHATROOM_NAME_LIMITS_IN_CHARS, + maxDescriptionLength: CHATROOM_DESCRIPTION_LIMITS_IN_CHARS + }, + attributes: { + creator: meta.username, + deletedDate: null, + archivedDate: null + }, + users: {}, + messages: [] + }, data); + for (const key in initialState) { + import_common2.Vue.set(state, key, initialState[key]); + } + } + }, + "gi.contracts/chatroom/join": { + validate: objectOf({ + username: string + }), + process({ data, meta, hash }, { state }) { + const { username } = data; + if (!state.saveMessage && state.users[username]) { + console.warn("Can not join the chatroom which you are already part of"); + return; + } + import_common2.Vue.set(state.users, username, { joinedDate: meta.createdDate }); + if (!state.saveMessage) { + return; + } + const notificationType = username === meta.username ? MESSAGE_NOTIFICATIONS.JOIN_MEMBER : MESSAGE_NOTIFICATIONS.ADD_MEMBER; + const notificationData = createNotificationData(notificationType, notificationType === MESSAGE_NOTIFICATIONS.ADD_MEMBER ? { username } : {}); + const newMessage = createMessage({ meta, hash, data: notificationData, state }); + state.messages.push(newMessage); + }, + sideEffect({ contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + if ((0, import_sbp3.default)("okTurtles.data/get", "READY_TO_JOIN_CHATROOM") || (0, import_sbp3.default)("okTurtles.data/get", "JOINING_CHATROOM_ID") === contractID) { + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + } + }, + "gi.contracts/chatroom/rename": { + validate: objectOf({ + name: string + }), + process({ data, meta, hash }, { state }) { + import_common2.Vue.set(state.attributes, "name", data.name); + if (!state.saveMessage) { + return; + } + const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_NAME, {}); + const newMessage = createMessage({ meta, hash, data: notificationData, state }); + state.messages.push(newMessage); + }, + sideEffect({ contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + if ((0, import_sbp3.default)("okTurtles.data/get", "READY_TO_JOIN_CHATROOM")) { + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + } + }, + "gi.contracts/chatroom/changeDescription": { + validate: objectOf({ + description: string + }), + process({ data, meta, hash }, { state }) { + import_common2.Vue.set(state.attributes, "description", data.description); + if (!state.saveMessage) { + return; + } + const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_DESCRIPTION, {}); + const newMessage = createMessage({ meta, hash, data: notificationData, state }); + state.messages.push(newMessage); + }, + sideEffect({ contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + if ((0, import_sbp3.default)("okTurtles.data/get", "READY_TO_JOIN_CHATROOM")) { + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + } + }, + "gi.contracts/chatroom/leave": { + validate: objectOf({ + username: optional(string), + member: string + }), + process({ data, meta, hash }, { state }) { + const { member } = data; + const isKicked = data.username && member !== data.username; + if (!state.saveMessage && !state.users[member]) { + throw new Error(`Can not leave the chatroom which ${member} are not part of`); + } + import_common2.Vue.delete(state.users, member); + if (!state.saveMessage) { + return; + } + const notificationType = !isKicked ? MESSAGE_NOTIFICATIONS.LEAVE_MEMBER : MESSAGE_NOTIFICATIONS.KICK_MEMBER; + const notificationData = createNotificationData(notificationType, isKicked ? { username: member } : {}); + const newMessage = createMessage({ + meta: isKicked ? meta : { ...meta, username: member }, + hash, + data: notificationData, + state + }); + state.messages.push(newMessage); + }, + sideEffect({ data, hash, contractID, meta }, { state }) { + const rootState = (0, import_sbp3.default)("state/vuex/state"); + if (data.member === rootState.loggedIn.username) { + if ((0, import_sbp3.default)("okTurtles.data/get", "READY_TO_JOIN_CHATROOM")) { + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + if ((0, import_sbp3.default)("okTurtles.data/get", "JOINING_CHATROOM_ID")) { + return; + } + leaveChatRoom({ contractID }); + } + emitMessageEvent({ contractID, hash }); + } + }, + "gi.contracts/chatroom/delete": { + validate: (data, { state, meta }) => { + if (state.attributes.creator !== meta.username) { + throw new TypeError((0, import_common2.L)("Only the channel creator can delete channel.")); + } + }, + process({ data, meta }, { state, rootState }) { + import_common2.Vue.set(state.attributes, "deletedDate", meta.createdDate); + for (const username in state.users) { + import_common2.Vue.delete(state.users, username); + } + }, + sideEffect({ meta, contractID }, { state }) { + if ((0, import_sbp3.default)("okTurtles.data/get", "JOINING_CHATROOM_ID")) { + return; + } + leaveChatRoom({ contractID }); + } + }, + "gi.contracts/chatroom/addMessage": { + validate: messageType, + process({ data, meta, hash }, { state }) { + if (!state.saveMessage) { + return; + } + const pendingMsg = state.messages.find((msg) => msg.id === hash && msg.pending); + if (pendingMsg) { + delete pendingMsg.pending; + } else { + state.messages.push(createMessage({ meta, data, hash, state })); + } + }, + sideEffect({ contractID, hash, meta, data }, { state, getters }) { + emitMessageEvent({ contractID, hash }); + const rootState = (0, import_sbp3.default)("state/vuex/state"); + const me = rootState.loggedIn.username; + if (me === meta.username) { + return; + } + const newMessage = createMessage({ meta, data, hash, state }); + const mentions = makeMentionFromUsername(me); + if (data.type === MESSAGE_TYPES.TEXT && (newMessage.text.includes(mentions.me) || newMessage.text.includes(mentions.all))) { + addMention({ + contractID, + messageId: newMessage.id, + datetime: newMessage.datetime, + text: newMessage.text, + username: meta.username, + chatRoomName: getters.chatRoomAttributes.name + }); + } + if ((0, import_sbp3.default)("okTurtles.data/get", "READY_TO_JOIN_CHATROOM")) { + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + } + }, + "gi.contracts/chatroom/editMessage": { + validate: (data, { state, meta }) => { + objectOf({ + id: string, + createdDate: string, + text: string + })(data); + }, + process({ data, meta }, { state }) { + if (!state.saveMessage) { + return; + } + const msgIndex = findMessageIdx(data.id, state.messages); + if (msgIndex >= 0 && meta.username === state.messages[msgIndex].from) { + state.messages[msgIndex].text = data.text; + state.messages[msgIndex].updatedDate = meta.createdDate; + if (state.saveMessage && state.messages[msgIndex].pending) { + delete state.messages[msgIndex].pending; + } + } + }, + sideEffect({ contractID, hash, meta, data }, { getters }) { + emitMessageEvent({ contractID, hash }); + const rootState = (0, import_sbp3.default)("state/vuex/state"); + const me = rootState.loggedIn.username; + if (me === meta.username) { + return; + } + const isAlreadyAdded = rootState.chatRoomUnread[contractID].mentions.find((m) => m.messageId === data.id); + const mentions = makeMentionFromUsername(me); + const isIncludeMention = data.text.includes(mentions.me) || data.text.includes(mentions.all); + if (!isAlreadyAdded && isIncludeMention) { + addMention({ + contractID, + messageId: data.id, + datetime: data.createdDate, + text: data.text, + username: meta.username, + chatRoomName: getters.chatRoomAttributes.name + }); + } else if (isAlreadyAdded && !isIncludeMention) { + deleteMention({ contractID, messageId: data.id }); + } + } + }, + "gi.contracts/chatroom/deleteMessage": { + validate: objectOf({ + id: string + }), + process({ data, meta }, { state }) { + if (!state.saveMessage) { + return; + } + const msgIndex = findMessageIdx(data.id, state.messages); + if (msgIndex >= 0) { + state.messages.splice(msgIndex, 1); + } + for (const message of state.messages) { + if (message.replyingMessage?.id === data.id) { + message.replyingMessage.id = null; + message.replyingMessage.text = (0, import_common2.L)("Original message was removed by {username}", { + username: makeMentionFromUsername(meta.username).me + }); + } + } + }, + sideEffect({ data, contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + const rootState = (0, import_sbp3.default)("state/vuex/state"); + const me = rootState.loggedIn.username; + if (rootState.chatRoomScrollPosition[contractID] === data.id) { + (0, import_sbp3.default)("state/vuex/commit", "setChatRoomScrollPosition", { + chatRoomId: contractID, + messageId: null + }); + } + if (rootState.chatRoomUnread[contractID].since.messageId === data.id) { + (0, import_sbp3.default)("state/vuex/commit", "deleteChatRoomUnreadSince", { + chatRoomId: contractID, + deletedDate: meta.createdDate + }); + } + if (me === meta.username) { + return; + } + if (rootState.chatRoomUnread[contractID].mentions.find((m) => m.messageId === data.id)) { + deleteMention({ contractID, messageId: data.id }); + } + emitMessageEvent({ contractID, hash }); + } + }, + "gi.contracts/chatroom/makeEmotion": { + validate: objectOf({ + id: string, + emoticon: string + }), + process({ data, meta, contractID }, { state }) { + if (!state.saveMessage) { + return; + } + const { id, emoticon } = data; + const msgIndex = findMessageIdx(id, state.messages); + if (msgIndex >= 0) { + let emoticons = cloneDeep(state.messages[msgIndex].emoticons || {}); + if (emoticons[emoticon]) { + const alreadyAdded = emoticons[emoticon].indexOf(meta.username); + if (alreadyAdded >= 0) { + emoticons[emoticon].splice(alreadyAdded, 1); + if (!emoticons[emoticon].length) { + delete emoticons[emoticon]; + if (!Object.keys(emoticons).length) { + emoticons = null; + } + } + } else { + emoticons[emoticon].push(meta.username); + } + } else { + emoticons[emoticon] = [meta.username]; + } + if (emoticons) { + import_common2.Vue.set(state.messages[msgIndex], "emoticons", emoticons); + } else { + import_common2.Vue.delete(state.messages[msgIndex], "emoticons"); + } + } + }, + sideEffect({ contractID, hash }) { + emitMessageEvent({ contractID, hash }); + } + } + } + }); +})(); diff --git a/contracts/0.0.8/chatroom.0.0.8.manifest.json b/contracts/0.0.8/chatroom.0.0.8.manifest.json new file mode 100644 index 0000000000..f69e6304d0 --- /dev/null +++ b/contracts/0.0.8/chatroom.0.0.8.manifest.json @@ -0,0 +1 @@ +{"head":{"manifestVersion":"1.0.0"},"body":"{\"version\":\"0.0.8\",\"contract\":{\"hash\":\"21XWnNNbmyRoxgWh1rgsGUc2BMouBnNexZ4WUYJZLo5Hfsyep6\",\"file\":\"chatroom.js\"},\"authors\":[{\"cipher\":\"algo\",\"key\":\"\"},{\"cipher\":\"algo\",\"key\":\"\"}],\"contractSlim\":{\"file\":\"chatroom-slim.js\",\"hash\":\"21XWnNHxRmhAxBPyr9aPB4VX1THB6d2S8qT1oGLREHNM4mNsuw\"}}","signature":{"key":"","signature":""}} \ No newline at end of file diff --git a/contracts/0.0.8/chatroom.js b/contracts/0.0.8/chatroom.js new file mode 100644 index 0000000000..556a87c5f4 --- /dev/null +++ b/contracts/0.0.8/chatroom.js @@ -0,0 +1,9897 @@ +"use strict"; +(() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { + get: (a, b) => (typeof require !== "undefined" ? require : a)[b] + }) : x)(function(x) { + if (typeof require !== "undefined") + return require.apply(this, arguments); + throw new Error('Dynamic require of "' + x + '" is not supported'); + }); + var __commonJS = (cb, mod) => function __require2() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target2, "default", { value: mod, enumerable: true }) : target2, mod)); + + // node_modules/dompurify/dist/purify.js + var require_purify = __commonJS({ + "node_modules/dompurify/dist/purify.js"(exports, module) { + (function(global2, factory) { + typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global2 = global2 || self, global2.DOMPurify = factory()); + })(exports, function() { + "use strict"; + function _toConsumableArray(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { + arr2[i] = arr[i]; + } + return arr2; + } else { + return Array.from(arr); + } + } + var hasOwnProperty2 = Object.hasOwnProperty, setPrototypeOf = Object.setPrototypeOf, isFrozen = Object.isFrozen, getPrototypeOf = Object.getPrototypeOf, getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + var freeze = Object.freeze, seal = Object.seal, create2 = Object.create; + var _ref = typeof Reflect !== "undefined" && Reflect, apply = _ref.apply, construct = _ref.construct; + if (!apply) { + apply = function apply2(fun, thisValue, args) { + return fun.apply(thisValue, args); + }; + } + if (!freeze) { + freeze = function freeze2(x) { + return x; + }; + } + if (!seal) { + seal = function seal2(x) { + return x; + }; + } + if (!construct) { + construct = function construct2(Func, args) { + return new (Function.prototype.bind.apply(Func, [null].concat(_toConsumableArray(args))))(); + }; + } + var arrayForEach = unapply(Array.prototype.forEach); + var arrayPop = unapply(Array.prototype.pop); + var arrayPush = unapply(Array.prototype.push); + var stringToLowerCase = unapply(String.prototype.toLowerCase); + var stringMatch = unapply(String.prototype.match); + var stringReplace = unapply(String.prototype.replace); + var stringIndexOf = unapply(String.prototype.indexOf); + var stringTrim = unapply(String.prototype.trim); + var regExpTest = unapply(RegExp.prototype.test); + var typeErrorCreate = unconstruct(TypeError); + function unapply(func) { + return function(thisArg) { + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + return apply(func, thisArg, args); + }; + } + function unconstruct(func) { + return function() { + for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + return construct(func, args); + }; + } + function addToSet(set2, array) { + if (setPrototypeOf) { + setPrototypeOf(set2, null); + } + var l = array.length; + while (l--) { + var element = array[l]; + if (typeof element === "string") { + var lcElement = stringToLowerCase(element); + if (lcElement !== element) { + if (!isFrozen(array)) { + array[l] = lcElement; + } + element = lcElement; + } + } + set2[element] = true; + } + return set2; + } + function clone(object2) { + var newObject = create2(null); + var property = void 0; + for (property in object2) { + if (apply(hasOwnProperty2, object2, [property])) { + newObject[property] = object2[property]; + } + } + return newObject; + } + function lookupGetter(object2, prop) { + while (object2 !== null) { + var desc = getOwnPropertyDescriptor(object2, prop); + if (desc) { + if (desc.get) { + return unapply(desc.get); + } + if (typeof desc.value === "function") { + return unapply(desc.value); + } + } + object2 = getPrototypeOf(object2); + } + function fallbackValue(element) { + console.warn("fallback value for", element); + return null; + } + return fallbackValue; + } + var html2 = freeze(["a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"]); + var svg = freeze(["svg", "a", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "style", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "view", "vkern"]); + var svgFilters = freeze(["feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence"]); + var svgDisallowed = freeze(["animate", "color-profile", "cursor", "discard", "fedropshadow", "feimage", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignobject", "hatch", "hatchpath", "mesh", "meshgradient", "meshpatch", "meshrow", "missing-glyph", "script", "set", "solidcolor", "unknown", "use"]); + var mathMl = freeze(["math", "menclose", "merror", "mfenced", "mfrac", "mglyph", "mi", "mlabeledtr", "mmultiscripts", "mn", "mo", "mover", "mpadded", "mphantom", "mroot", "mrow", "ms", "mspace", "msqrt", "mstyle", "msub", "msup", "msubsup", "mtable", "mtd", "mtext", "mtr", "munder", "munderover"]); + var mathMlDisallowed = freeze(["maction", "maligngroup", "malignmark", "mlongdiv", "mscarries", "mscarry", "msgroup", "mstack", "msline", "msrow", "semantics", "annotation", "annotation-xml", "mprescripts", "none"]); + var text2 = freeze(["#text"]); + var html$1 = freeze(["accept", "action", "align", "alt", "autocapitalize", "autocomplete", "autopictureinpicture", "autoplay", "background", "bgcolor", "border", "capture", "cellpadding", "cellspacing", "checked", "cite", "class", "clear", "color", "cols", "colspan", "controls", "controlslist", "coords", "crossorigin", "datetime", "decoding", "default", "dir", "disabled", "disablepictureinpicture", "disableremoteplayback", "download", "draggable", "enctype", "enterkeyhint", "face", "for", "headers", "height", "hidden", "high", "href", "hreflang", "id", "inputmode", "integrity", "ismap", "kind", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "minlength", "multiple", "muted", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "playsinline", "poster", "preload", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected", "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "title", "translate", "type", "usemap", "valign", "value", "width", "xmlns"]); + var svg$1 = freeze(["accent-height", "accumulate", "additive", "alignment-baseline", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clippathunits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill", "fill-opacity", "fill-rule", "filter", "filterunits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "primitiveunits", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "specularconstant", "specularexponent", "spreadmethod", "startoffset", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "systemlanguage", "tabindex", "targetx", "targety", "transform", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xmlns", "y", "y1", "y2", "z", "zoomandpan"]); + var mathMl$1 = freeze(["accent", "accentunder", "align", "bevelled", "close", "columnsalign", "columnlines", "columnspan", "denomalign", "depth", "dir", "display", "displaystyle", "encoding", "fence", "frame", "height", "href", "id", "largeop", "length", "linethickness", "lspace", "lquote", "mathbackground", "mathcolor", "mathsize", "mathvariant", "maxsize", "minsize", "movablelimits", "notation", "numalign", "open", "rowalign", "rowlines", "rowspacing", "rowspan", "rspace", "rquote", "scriptlevel", "scriptminsize", "scriptsizemultiplier", "selection", "separator", "separators", "stretchy", "subscriptshift", "supscriptshift", "symmetric", "voffset", "width", "xmlns"]); + var xml = freeze(["xlink:href", "xml:id", "xlink:title", "xml:space", "xmlns:xlink"]); + var MUSTACHE_EXPR = seal(/\{\{[\s\S]*|[\s\S]*\}\}/gm); + var ERB_EXPR = seal(/<%[\s\S]*|[\s\S]*%>/gm); + var DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); + var ARIA_ATTR = seal(/^aria-[\-\w]+$/); + var IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i); + var IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); + var ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g); + var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) { + return typeof obj; + } : function(obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + function _toConsumableArray$1(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { + arr2[i] = arr[i]; + } + return arr2; + } else { + return Array.from(arr); + } + } + var getGlobal = function getGlobal2() { + return typeof window === "undefined" ? null : window; + }; + var _createTrustedTypesPolicy = function _createTrustedTypesPolicy2(trustedTypes, document2) { + if ((typeof trustedTypes === "undefined" ? "undefined" : _typeof(trustedTypes)) !== "object" || typeof trustedTypes.createPolicy !== "function") { + return null; + } + var suffix = null; + var ATTR_NAME = "data-tt-policy-suffix"; + if (document2.currentScript && document2.currentScript.hasAttribute(ATTR_NAME)) { + suffix = document2.currentScript.getAttribute(ATTR_NAME); + } + var policyName = "dompurify" + (suffix ? "#" + suffix : ""); + try { + return trustedTypes.createPolicy(policyName, { + createHTML: function createHTML(html$$1) { + return html$$1; + } + }); + } catch (_) { + console.warn("TrustedTypes policy " + policyName + " could not be created."); + return null; + } + }; + function createDOMPurify() { + var window2 = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal(); + var DOMPurify = function DOMPurify2(root) { + return createDOMPurify(root); + }; + DOMPurify.version = "2.2.7"; + DOMPurify.removed = []; + if (!window2 || !window2.document || window2.document.nodeType !== 9) { + DOMPurify.isSupported = false; + return DOMPurify; + } + var originalDocument = window2.document; + var document2 = window2.document; + var DocumentFragment = window2.DocumentFragment, HTMLTemplateElement = window2.HTMLTemplateElement, Node = window2.Node, Element = window2.Element, NodeFilter = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap, NamedNodeMap = _window$NamedNodeMap === void 0 ? window2.NamedNodeMap || window2.MozNamedAttrMap : _window$NamedNodeMap, Text = window2.Text, Comment = window2.Comment, DOMParser = window2.DOMParser, trustedTypes = window2.trustedTypes; + var ElementPrototype = Element.prototype; + var cloneNode = lookupGetter(ElementPrototype, "cloneNode"); + var getNextSibling = lookupGetter(ElementPrototype, "nextSibling"); + var getChildNodes = lookupGetter(ElementPrototype, "childNodes"); + var getParentNode = lookupGetter(ElementPrototype, "parentNode"); + if (typeof HTMLTemplateElement === "function") { + var template2 = document2.createElement("template"); + if (template2.content && template2.content.ownerDocument) { + document2 = template2.content.ownerDocument; + } + } + var trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, originalDocument); + var emptyHTML = trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML("") : ""; + var _document = document2, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, getElementsByTagName = _document.getElementsByTagName, createDocumentFragment = _document.createDocumentFragment; + var importNode = originalDocument.importNode; + var documentMode = {}; + try { + documentMode = clone(document2).documentMode ? document2.documentMode : {}; + } catch (_) { + } + var hooks2 = {}; + DOMPurify.isSupported = typeof getParentNode === "function" && implementation && typeof implementation.createHTMLDocument !== "undefined" && documentMode !== 9; + var MUSTACHE_EXPR$$1 = MUSTACHE_EXPR, ERB_EXPR$$1 = ERB_EXPR, DATA_ATTR$$1 = DATA_ATTR, ARIA_ATTR$$1 = ARIA_ATTR, IS_SCRIPT_OR_DATA$$1 = IS_SCRIPT_OR_DATA, ATTR_WHITESPACE$$1 = ATTR_WHITESPACE; + var IS_ALLOWED_URI$$1 = IS_ALLOWED_URI; + var ALLOWED_TAGS = null; + var DEFAULT_ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(html2), _toConsumableArray$1(svg), _toConsumableArray$1(svgFilters), _toConsumableArray$1(mathMl), _toConsumableArray$1(text2))); + var ALLOWED_ATTR = null; + var DEFAULT_ALLOWED_ATTR = addToSet({}, [].concat(_toConsumableArray$1(html$1), _toConsumableArray$1(svg$1), _toConsumableArray$1(mathMl$1), _toConsumableArray$1(xml))); + var FORBID_TAGS = null; + var FORBID_ATTR = null; + var ALLOW_ARIA_ATTR = true; + var ALLOW_DATA_ATTR = true; + var ALLOW_UNKNOWN_PROTOCOLS = false; + var SAFE_FOR_TEMPLATES = false; + var WHOLE_DOCUMENT = false; + var SET_CONFIG = false; + var FORCE_BODY = false; + var RETURN_DOM = false; + var RETURN_DOM_FRAGMENT = false; + var RETURN_DOM_IMPORT = true; + var RETURN_TRUSTED_TYPE = false; + var SANITIZE_DOM = true; + var KEEP_CONTENT = true; + var IN_PLACE = false; + var USE_PROFILES = {}; + var FORBID_CONTENTS = addToSet({}, ["annotation-xml", "audio", "colgroup", "desc", "foreignobject", "head", "iframe", "math", "mi", "mn", "mo", "ms", "mtext", "noembed", "noframes", "noscript", "plaintext", "script", "style", "svg", "template", "thead", "title", "video", "xmp"]); + var DATA_URI_TAGS = null; + var DEFAULT_DATA_URI_TAGS = addToSet({}, ["audio", "video", "img", "source", "image", "track"]); + var URI_SAFE_ATTRIBUTES = null; + var DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ["alt", "class", "for", "id", "label", "name", "pattern", "placeholder", "summary", "title", "value", "style", "xmlns"]); + var CONFIG = null; + var formElement = document2.createElement("form"); + var _parseConfig = function _parseConfig2(cfg) { + if (CONFIG && CONFIG === cfg) { + return; + } + if (!cfg || (typeof cfg === "undefined" ? "undefined" : _typeof(cfg)) !== "object") { + cfg = {}; + } + cfg = clone(cfg); + ALLOWED_TAGS = "ALLOWED_TAGS" in cfg ? addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS; + ALLOWED_ATTR = "ALLOWED_ATTR" in cfg ? addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR; + URI_SAFE_ATTRIBUTES = "ADD_URI_SAFE_ATTR" in cfg ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR) : DEFAULT_URI_SAFE_ATTRIBUTES; + DATA_URI_TAGS = "ADD_DATA_URI_TAGS" in cfg ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS) : DEFAULT_DATA_URI_TAGS; + FORBID_TAGS = "FORBID_TAGS" in cfg ? addToSet({}, cfg.FORBID_TAGS) : {}; + FORBID_ATTR = "FORBID_ATTR" in cfg ? addToSet({}, cfg.FORBID_ATTR) : {}; + USE_PROFILES = "USE_PROFILES" in cfg ? cfg.USE_PROFILES : false; + ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; + ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; + ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; + SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; + WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; + RETURN_DOM = cfg.RETURN_DOM || false; + RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; + RETURN_DOM_IMPORT = cfg.RETURN_DOM_IMPORT !== false; + RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; + FORCE_BODY = cfg.FORCE_BODY || false; + SANITIZE_DOM = cfg.SANITIZE_DOM !== false; + KEEP_CONTENT = cfg.KEEP_CONTENT !== false; + IN_PLACE = cfg.IN_PLACE || false; + IS_ALLOWED_URI$$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI$$1; + if (SAFE_FOR_TEMPLATES) { + ALLOW_DATA_ATTR = false; + } + if (RETURN_DOM_FRAGMENT) { + RETURN_DOM = true; + } + if (USE_PROFILES) { + ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(text2))); + ALLOWED_ATTR = []; + if (USE_PROFILES.html === true) { + addToSet(ALLOWED_TAGS, html2); + addToSet(ALLOWED_ATTR, html$1); + } + if (USE_PROFILES.svg === true) { + addToSet(ALLOWED_TAGS, svg); + addToSet(ALLOWED_ATTR, svg$1); + addToSet(ALLOWED_ATTR, xml); + } + if (USE_PROFILES.svgFilters === true) { + addToSet(ALLOWED_TAGS, svgFilters); + addToSet(ALLOWED_ATTR, svg$1); + addToSet(ALLOWED_ATTR, xml); + } + if (USE_PROFILES.mathMl === true) { + addToSet(ALLOWED_TAGS, mathMl); + addToSet(ALLOWED_ATTR, mathMl$1); + addToSet(ALLOWED_ATTR, xml); + } + } + if (cfg.ADD_TAGS) { + if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { + ALLOWED_TAGS = clone(ALLOWED_TAGS); + } + addToSet(ALLOWED_TAGS, cfg.ADD_TAGS); + } + if (cfg.ADD_ATTR) { + if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { + ALLOWED_ATTR = clone(ALLOWED_ATTR); + } + addToSet(ALLOWED_ATTR, cfg.ADD_ATTR); + } + if (cfg.ADD_URI_SAFE_ATTR) { + addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR); + } + if (KEEP_CONTENT) { + ALLOWED_TAGS["#text"] = true; + } + if (WHOLE_DOCUMENT) { + addToSet(ALLOWED_TAGS, ["html", "head", "body"]); + } + if (ALLOWED_TAGS.table) { + addToSet(ALLOWED_TAGS, ["tbody"]); + delete FORBID_TAGS.tbody; + } + if (freeze) { + freeze(cfg); + } + CONFIG = cfg; + }; + var MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ["mi", "mo", "mn", "ms", "mtext"]); + var HTML_INTEGRATION_POINTS = addToSet({}, ["foreignobject", "desc", "title", "annotation-xml"]); + var ALL_SVG_TAGS = addToSet({}, svg); + addToSet(ALL_SVG_TAGS, svgFilters); + addToSet(ALL_SVG_TAGS, svgDisallowed); + var ALL_MATHML_TAGS = addToSet({}, mathMl); + addToSet(ALL_MATHML_TAGS, mathMlDisallowed); + var MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML"; + var SVG_NAMESPACE = "http://www.w3.org/2000/svg"; + var HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; + var _checkValidNamespace = function _checkValidNamespace2(element) { + var parent = getParentNode(element); + if (!parent || !parent.tagName) { + parent = { + namespaceURI: HTML_NAMESPACE, + tagName: "template" + }; + } + var tagName2 = stringToLowerCase(element.tagName); + var parentTagName = stringToLowerCase(parent.tagName); + if (element.namespaceURI === SVG_NAMESPACE) { + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName2 === "svg"; + } + if (parent.namespaceURI === MATHML_NAMESPACE) { + return tagName2 === "svg" && (parentTagName === "annotation-xml" || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); + } + return Boolean(ALL_SVG_TAGS[tagName2]); + } + if (element.namespaceURI === MATHML_NAMESPACE) { + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName2 === "math"; + } + if (parent.namespaceURI === SVG_NAMESPACE) { + return tagName2 === "math" && HTML_INTEGRATION_POINTS[parentTagName]; + } + return Boolean(ALL_MATHML_TAGS[tagName2]); + } + if (element.namespaceURI === HTML_NAMESPACE) { + if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { + return false; + } + if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { + return false; + } + var commonSvgAndHTMLElements = addToSet({}, ["title", "style", "font", "a", "script"]); + return !ALL_MATHML_TAGS[tagName2] && (commonSvgAndHTMLElements[tagName2] || !ALL_SVG_TAGS[tagName2]); + } + return false; + }; + var _forceRemove = function _forceRemove2(node) { + arrayPush(DOMPurify.removed, { element: node }); + try { + node.parentNode.removeChild(node); + } catch (_) { + try { + node.outerHTML = emptyHTML; + } catch (_2) { + node.remove(); + } + } + }; + var _removeAttribute = function _removeAttribute2(name, node) { + try { + arrayPush(DOMPurify.removed, { + attribute: node.getAttributeNode(name), + from: node + }); + } catch (_) { + arrayPush(DOMPurify.removed, { + attribute: null, + from: node + }); + } + node.removeAttribute(name); + if (name === "is" && !ALLOWED_ATTR[name]) { + if (RETURN_DOM || RETURN_DOM_FRAGMENT) { + try { + _forceRemove(node); + } catch (_) { + } + } else { + try { + node.setAttribute(name, ""); + } catch (_) { + } + } + } + }; + var _initDocument = function _initDocument2(dirty) { + var doc = void 0; + var leadingWhitespace = void 0; + if (FORCE_BODY) { + dirty = "" + dirty; + } else { + var matches2 = stringMatch(dirty, /^[\r\n\t ]+/); + leadingWhitespace = matches2 && matches2[0]; + } + var dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; + try { + doc = new DOMParser().parseFromString(dirtyPayload, "text/html"); + } catch (_) { + } + if (!doc || !doc.documentElement) { + doc = implementation.createHTMLDocument(""); + var _doc = doc, body = _doc.body; + body.parentNode.removeChild(body.parentNode.firstElementChild); + body.outerHTML = dirtyPayload; + } + if (dirty && leadingWhitespace) { + doc.body.insertBefore(document2.createTextNode(leadingWhitespace), doc.body.childNodes[0] || null); + } + return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? "html" : "body")[0]; + }; + var _createIterator = function _createIterator2(root) { + return createNodeIterator.call(root.ownerDocument || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, function() { + return NodeFilter.FILTER_ACCEPT; + }, false); + }; + var _isClobbered = function _isClobbered2(elm) { + if (elm instanceof Text || elm instanceof Comment) { + return false; + } + if (typeof elm.nodeName !== "string" || typeof elm.textContent !== "string" || typeof elm.removeChild !== "function" || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== "function" || typeof elm.setAttribute !== "function" || typeof elm.namespaceURI !== "string" || typeof elm.insertBefore !== "function") { + return true; + } + return false; + }; + var _isNode = function _isNode2(object2) { + return (typeof Node === "undefined" ? "undefined" : _typeof(Node)) === "object" ? object2 instanceof Node : object2 && (typeof object2 === "undefined" ? "undefined" : _typeof(object2)) === "object" && typeof object2.nodeType === "number" && typeof object2.nodeName === "string"; + }; + var _executeHook = function _executeHook2(entryPoint, currentNode, data) { + if (!hooks2[entryPoint]) { + return; + } + arrayForEach(hooks2[entryPoint], function(hook) { + hook.call(DOMPurify, currentNode, data, CONFIG); + }); + }; + var _sanitizeElements = function _sanitizeElements2(currentNode) { + var content = void 0; + _executeHook("beforeSanitizeElements", currentNode, null); + if (_isClobbered(currentNode)) { + _forceRemove(currentNode); + return true; + } + if (stringMatch(currentNode.nodeName, /[\u0080-\uFFFF]/)) { + _forceRemove(currentNode); + return true; + } + var tagName2 = stringToLowerCase(currentNode.nodeName); + _executeHook("uponSanitizeElement", currentNode, { + tagName: tagName2, + allowedTags: ALLOWED_TAGS + }); + if (!_isNode(currentNode.firstElementChild) && (!_isNode(currentNode.content) || !_isNode(currentNode.content.firstElementChild)) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) { + _forceRemove(currentNode); + return true; + } + if (!ALLOWED_TAGS[tagName2] || FORBID_TAGS[tagName2]) { + if (KEEP_CONTENT && !FORBID_CONTENTS[tagName2]) { + var parentNode2 = getParentNode(currentNode); + var childNodes = getChildNodes(currentNode); + if (childNodes && parentNode2) { + var childCount = childNodes.length; + for (var i = childCount - 1; i >= 0; --i) { + parentNode2.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode)); + } + } + } + _forceRemove(currentNode); + return true; + } + if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) { + _forceRemove(currentNode); + return true; + } + if ((tagName2 === "noscript" || tagName2 === "noembed") && regExpTest(/<\/no(script|embed)/i, currentNode.innerHTML)) { + _forceRemove(currentNode); + return true; + } + if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) { + content = currentNode.textContent; + content = stringReplace(content, MUSTACHE_EXPR$$1, " "); + content = stringReplace(content, ERB_EXPR$$1, " "); + if (currentNode.textContent !== content) { + arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() }); + currentNode.textContent = content; + } + } + _executeHook("afterSanitizeElements", currentNode, null); + return false; + }; + var _isValidAttribute = function _isValidAttribute2(lcTag, lcName, value) { + if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document2 || value in formElement)) { + return false; + } + if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$$1, lcName)) + ; + else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$$1, lcName)) + ; + else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) { + return false; + } else if (URI_SAFE_ATTRIBUTES[lcName]) + ; + else if (regExpTest(IS_ALLOWED_URI$$1, stringReplace(value, ATTR_WHITESPACE$$1, ""))) + ; + else if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]) + ; + else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$$1, stringReplace(value, ATTR_WHITESPACE$$1, ""))) + ; + else if (!value) + ; + else { + return false; + } + return true; + }; + var _sanitizeAttributes = function _sanitizeAttributes2(currentNode) { + var attr = void 0; + var value = void 0; + var lcName = void 0; + var l = void 0; + _executeHook("beforeSanitizeAttributes", currentNode, null); + var attributes = currentNode.attributes; + if (!attributes) { + return; + } + var hookEvent = { + attrName: "", + attrValue: "", + keepAttr: true, + allowedAttributes: ALLOWED_ATTR + }; + l = attributes.length; + while (l--) { + attr = attributes[l]; + var _attr = attr, name = _attr.name, namespaceURI = _attr.namespaceURI; + value = stringTrim(attr.value); + lcName = stringToLowerCase(name); + hookEvent.attrName = lcName; + hookEvent.attrValue = value; + hookEvent.keepAttr = true; + hookEvent.forceKeepAttr = void 0; + _executeHook("uponSanitizeAttribute", currentNode, hookEvent); + value = hookEvent.attrValue; + if (hookEvent.forceKeepAttr) { + continue; + } + _removeAttribute(name, currentNode); + if (!hookEvent.keepAttr) { + continue; + } + if (regExpTest(/\/>/i, value)) { + _removeAttribute(name, currentNode); + continue; + } + if (SAFE_FOR_TEMPLATES) { + value = stringReplace(value, MUSTACHE_EXPR$$1, " "); + value = stringReplace(value, ERB_EXPR$$1, " "); + } + var lcTag = currentNode.nodeName.toLowerCase(); + if (!_isValidAttribute(lcTag, lcName, value)) { + continue; + } + try { + if (namespaceURI) { + currentNode.setAttributeNS(namespaceURI, name, value); + } else { + currentNode.setAttribute(name, value); + } + arrayPop(DOMPurify.removed); + } catch (_) { + } + } + _executeHook("afterSanitizeAttributes", currentNode, null); + }; + var _sanitizeShadowDOM = function _sanitizeShadowDOM2(fragment) { + var shadowNode = void 0; + var shadowIterator = _createIterator(fragment); + _executeHook("beforeSanitizeShadowDOM", fragment, null); + while (shadowNode = shadowIterator.nextNode()) { + _executeHook("uponSanitizeShadowNode", shadowNode, null); + if (_sanitizeElements(shadowNode)) { + continue; + } + if (shadowNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM2(shadowNode.content); + } + _sanitizeAttributes(shadowNode); + } + _executeHook("afterSanitizeShadowDOM", fragment, null); + }; + DOMPurify.sanitize = function(dirty, cfg) { + var body = void 0; + var importedNode = void 0; + var currentNode = void 0; + var oldNode = void 0; + var returnNode = void 0; + if (!dirty) { + dirty = ""; + } + if (typeof dirty !== "string" && !_isNode(dirty)) { + if (typeof dirty.toString !== "function") { + throw typeErrorCreate("toString is not a function"); + } else { + dirty = dirty.toString(); + if (typeof dirty !== "string") { + throw typeErrorCreate("dirty is not a string, aborting"); + } + } + } + if (!DOMPurify.isSupported) { + if (_typeof(window2.toStaticHTML) === "object" || typeof window2.toStaticHTML === "function") { + if (typeof dirty === "string") { + return window2.toStaticHTML(dirty); + } + if (_isNode(dirty)) { + return window2.toStaticHTML(dirty.outerHTML); + } + } + return dirty; + } + if (!SET_CONFIG) { + _parseConfig(cfg); + } + DOMPurify.removed = []; + if (typeof dirty === "string") { + IN_PLACE = false; + } + if (IN_PLACE) + ; + else if (dirty instanceof Node) { + body = _initDocument(""); + importedNode = body.ownerDocument.importNode(dirty, true); + if (importedNode.nodeType === 1 && importedNode.nodeName === "BODY") { + body = importedNode; + } else if (importedNode.nodeName === "HTML") { + body = importedNode; + } else { + body.appendChild(importedNode); + } + } else { + if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && dirty.indexOf("<") === -1) { + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty; + } + body = _initDocument(dirty); + if (!body) { + return RETURN_DOM ? null : emptyHTML; + } + } + if (body && FORCE_BODY) { + _forceRemove(body.firstChild); + } + var nodeIterator = _createIterator(IN_PLACE ? dirty : body); + while (currentNode = nodeIterator.nextNode()) { + if (currentNode.nodeType === 3 && currentNode === oldNode) { + continue; + } + if (_sanitizeElements(currentNode)) { + continue; + } + if (currentNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(currentNode.content); + } + _sanitizeAttributes(currentNode); + oldNode = currentNode; + } + oldNode = null; + if (IN_PLACE) { + return dirty; + } + if (RETURN_DOM) { + if (RETURN_DOM_FRAGMENT) { + returnNode = createDocumentFragment.call(body.ownerDocument); + while (body.firstChild) { + returnNode.appendChild(body.firstChild); + } + } else { + returnNode = body; + } + if (RETURN_DOM_IMPORT) { + returnNode = importNode.call(originalDocument, returnNode, true); + } + return returnNode; + } + var serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; + if (SAFE_FOR_TEMPLATES) { + serializedHTML = stringReplace(serializedHTML, MUSTACHE_EXPR$$1, " "); + serializedHTML = stringReplace(serializedHTML, ERB_EXPR$$1, " "); + } + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML; + }; + DOMPurify.setConfig = function(cfg) { + _parseConfig(cfg); + SET_CONFIG = true; + }; + DOMPurify.clearConfig = function() { + CONFIG = null; + SET_CONFIG = false; + }; + DOMPurify.isValidAttribute = function(tag, attr, value) { + if (!CONFIG) { + _parseConfig({}); + } + var lcTag = stringToLowerCase(tag); + var lcName = stringToLowerCase(attr); + return _isValidAttribute(lcTag, lcName, value); + }; + DOMPurify.addHook = function(entryPoint, hookFunction) { + if (typeof hookFunction !== "function") { + return; + } + hooks2[entryPoint] = hooks2[entryPoint] || []; + arrayPush(hooks2[entryPoint], hookFunction); + }; + DOMPurify.removeHook = function(entryPoint) { + if (hooks2[entryPoint]) { + arrayPop(hooks2[entryPoint]); + } + }; + DOMPurify.removeHooks = function(entryPoint) { + if (hooks2[entryPoint]) { + hooks2[entryPoint] = []; + } + }; + DOMPurify.removeAllHooks = function() { + hooks2 = {}; + }; + return DOMPurify; + } + var purify = createDOMPurify(); + return purify; + }); + } + }); + + // frontend/model/contracts/chatroom.js + var import_sbp4 = __toESM(__require("@sbp/sbp")); + + // node_modules/vue/dist/vue.esm.js + var emptyObject = Object.freeze({}); + function isUndef(v) { + return v === void 0 || v === null; + } + function isDef(v) { + return v !== void 0 && v !== null; + } + function isTrue(v) { + return v === true; + } + function isFalse(v) { + return v === false; + } + function isPrimitive(value) { + return typeof value === "string" || typeof value === "number" || typeof value === "symbol" || typeof value === "boolean"; + } + function isObject(obj) { + return obj !== null && typeof obj === "object"; + } + var _toString = Object.prototype.toString; + function toRawType(value) { + return _toString.call(value).slice(8, -1); + } + function isPlainObject(obj) { + return _toString.call(obj) === "[object Object]"; + } + function isRegExp(v) { + return _toString.call(v) === "[object RegExp]"; + } + function isValidArrayIndex(val) { + var n = parseFloat(String(val)); + return n >= 0 && Math.floor(n) === n && isFinite(val); + } + function isPromise(val) { + return isDef(val) && typeof val.then === "function" && typeof val.catch === "function"; + } + function toString(val) { + return val == null ? "" : Array.isArray(val) || isPlainObject(val) && val.toString === _toString ? JSON.stringify(val, null, 2) : String(val); + } + function toNumber(val) { + var n = parseFloat(val); + return isNaN(n) ? val : n; + } + function makeMap(str2, expectsLowerCase) { + var map = /* @__PURE__ */ Object.create(null); + var list = str2.split(","); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase ? function(val) { + return map[val.toLowerCase()]; + } : function(val) { + return map[val]; + }; + } + var isBuiltInTag = makeMap("slot,component", true); + var isReservedAttribute = makeMap("key,ref,slot,slot-scope,is"); + function remove(arr, item) { + if (arr.length) { + var index2 = arr.indexOf(item); + if (index2 > -1) { + return arr.splice(index2, 1); + } + } + } + var hasOwnProperty = Object.prototype.hasOwnProperty; + function hasOwn(obj, key) { + return hasOwnProperty.call(obj, key); + } + function cached(fn) { + var cache = /* @__PURE__ */ Object.create(null); + return function cachedFn(str2) { + var hit = cache[str2]; + return hit || (cache[str2] = fn(str2)); + }; + } + var camelizeRE = /-(\w)/g; + var camelize = cached(function(str2) { + return str2.replace(camelizeRE, function(_, c) { + return c ? c.toUpperCase() : ""; + }); + }); + var capitalize = cached(function(str2) { + return str2.charAt(0).toUpperCase() + str2.slice(1); + }); + var hyphenateRE = /\B([A-Z])/g; + var hyphenate = cached(function(str2) { + return str2.replace(hyphenateRE, "-$1").toLowerCase(); + }); + function polyfillBind(fn, ctx) { + function boundFn(a) { + var l = arguments.length; + return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx); + } + boundFn._length = fn.length; + return boundFn; + } + function nativeBind(fn, ctx) { + return fn.bind(ctx); + } + var bind = Function.prototype.bind ? nativeBind : polyfillBind; + function toArray(list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret; + } + function extend(to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to; + } + function toObject(arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res; + } + function noop(a, b, c) { + } + var no = function(a, b, c) { + return false; + }; + var identity = function(_) { + return _; + }; + function genStaticKeys(modules2) { + return modules2.reduce(function(keys, m) { + return keys.concat(m.staticKeys || []); + }, []).join(","); + } + function looseEqual(a, b) { + if (a === b) { + return true; + } + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return a.length === b.length && a.every(function(e, i) { + return looseEqual(e, b[i]); + }); + } else if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime(); + } else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function(key) { + return looseEqual(a[key], b[key]); + }); + } else { + return false; + } + } catch (e) { + return false; + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b); + } else { + return false; + } + } + function looseIndexOf(arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) { + return i; + } + } + return -1; + } + function once(fn) { + var called = false; + return function() { + if (!called) { + called = true; + fn.apply(this, arguments); + } + }; + } + var SSR_ATTR = "data-server-rendered"; + var ASSET_TYPES = [ + "component", + "directive", + "filter" + ]; + var LIFECYCLE_HOOKS = [ + "beforeCreate", + "created", + "beforeMount", + "mounted", + "beforeUpdate", + "updated", + "beforeDestroy", + "destroyed", + "activated", + "deactivated", + "errorCaptured", + "serverPrefetch" + ]; + var config = { + optionMergeStrategies: /* @__PURE__ */ Object.create(null), + silent: false, + productionTip: true, + devtools: true, + performance: false, + errorHandler: null, + warnHandler: null, + ignoredElements: [], + keyCodes: /* @__PURE__ */ Object.create(null), + isReservedTag: no, + isReservedAttr: no, + isUnknownElement: no, + getTagNamespace: noop, + parsePlatformTagName: identity, + mustUseProp: no, + async: true, + _lifecycleHooks: LIFECYCLE_HOOKS + }; + var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; + function isReserved(str2) { + var c = (str2 + "").charCodeAt(0); + return c === 36 || c === 95; + } + function def(obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); + } + var bailRE = new RegExp("[^" + unicodeRegExp.source + ".$_\\d]"); + function parsePath(path) { + if (bailRE.test(path)) { + return; + } + var segments = path.split("."); + return function(obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) { + return; + } + obj = obj[segments[i]]; + } + return obj; + }; + } + var hasProto = "__proto__" in {}; + var inBrowser = typeof window !== "undefined"; + var inWeex = typeof WXEnvironment !== "undefined" && !!WXEnvironment.platform; + var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); + var UA = inBrowser && window.navigator.userAgent.toLowerCase(); + var isIE = UA && /msie|trident/.test(UA); + var isIE9 = UA && UA.indexOf("msie 9.0") > 0; + var isEdge = UA && UA.indexOf("edge/") > 0; + var isAndroid = UA && UA.indexOf("android") > 0 || weexPlatform === "android"; + var isIOS = UA && /iphone|ipad|ipod|ios/.test(UA) || weexPlatform === "ios"; + var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; + var isPhantomJS = UA && /phantomjs/.test(UA); + var isFF = UA && UA.match(/firefox\/(\d+)/); + var nativeWatch = {}.watch; + var supportsPassive = false; + if (inBrowser) { + try { + opts = {}; + Object.defineProperty(opts, "passive", { + get: function get3() { + supportsPassive = true; + } + }); + window.addEventListener("test-passive", null, opts); + } catch (e) { + } + } + var opts; + var _isServer; + var isServerRendering = function() { + if (_isServer === void 0) { + if (!inBrowser && !inWeex && typeof global !== "undefined") { + _isServer = global["process"] && global["process"].env.VUE_ENV === "server"; + } else { + _isServer = false; + } + } + return _isServer; + }; + var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + function isNative(Ctor) { + return typeof Ctor === "function" && /native code/.test(Ctor.toString()); + } + var hasSymbol = typeof Symbol !== "undefined" && isNative(Symbol) && typeof Reflect !== "undefined" && isNative(Reflect.ownKeys); + var _Set; + if (typeof Set !== "undefined" && isNative(Set)) { + _Set = Set; + } else { + _Set = /* @__PURE__ */ function() { + function Set2() { + this.set = /* @__PURE__ */ Object.create(null); + } + Set2.prototype.has = function has2(key) { + return this.set[key] === true; + }; + Set2.prototype.add = function add2(key) { + this.set[key] = true; + }; + Set2.prototype.clear = function clear() { + this.set = /* @__PURE__ */ Object.create(null); + }; + return Set2; + }(); + } + var warn = noop; + var tip = noop; + var generateComponentTrace = noop; + var formatComponentName = noop; + if (true) { + hasConsole = typeof console !== "undefined"; + classifyRE = /(?:^|[-_])(\w)/g; + classify = function(str2) { + return str2.replace(classifyRE, function(c) { + return c.toUpperCase(); + }).replace(/[-_]/g, ""); + }; + warn = function(msg, vm) { + var trace = vm ? generateComponentTrace(vm) : ""; + if (config.warnHandler) { + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && !config.silent) { + console.error("[Vue warn]: " + msg + trace); + } + }; + tip = function(msg, vm) { + if (hasConsole && !config.silent) { + console.warn("[Vue tip]: " + msg + (vm ? generateComponentTrace(vm) : "")); + } + }; + formatComponentName = function(vm, includeFile) { + if (vm.$root === vm) { + return ""; + } + var options = typeof vm === "function" && vm.cid != null ? vm.options : vm._isVue ? vm.$options || vm.constructor.options : vm; + var name = options.name || options._componentTag; + var file = options.__file; + if (!name && file) { + var match = file.match(/([^/\\]+)\.vue$/); + name = match && match[1]; + } + return (name ? "<" + classify(name) + ">" : "") + (file && includeFile !== false ? " at " + file : ""); + }; + repeat = function(str2, n) { + var res = ""; + while (n) { + if (n % 2 === 1) { + res += str2; + } + if (n > 1) { + str2 += str2; + } + n >>= 1; + } + return res; + }; + generateComponentTrace = function(vm) { + if (vm._isVue && vm.$parent) { + var tree = []; + var currentRecursiveSequence = 0; + while (vm) { + if (tree.length > 0) { + var last = tree[tree.length - 1]; + if (last.constructor === vm.constructor) { + currentRecursiveSequence++; + vm = vm.$parent; + continue; + } else if (currentRecursiveSequence > 0) { + tree[tree.length - 1] = [last, currentRecursiveSequence]; + currentRecursiveSequence = 0; + } + } + tree.push(vm); + vm = vm.$parent; + } + return "\n\nfound in\n\n" + tree.map(function(vm2, i) { + return "" + (i === 0 ? "---> " : repeat(" ", 5 + i * 2)) + (Array.isArray(vm2) ? formatComponentName(vm2[0]) + "... (" + vm2[1] + " recursive calls)" : formatComponentName(vm2)); + }).join("\n"); + } else { + return "\n\n(found in " + formatComponentName(vm) + ")"; + } + }; + } + var hasConsole; + var classifyRE; + var classify; + var repeat; + var uid = 0; + var Dep = function Dep2() { + this.id = uid++; + this.subs = []; + }; + Dep.prototype.addSub = function addSub(sub) { + this.subs.push(sub); + }; + Dep.prototype.removeSub = function removeSub(sub) { + remove(this.subs, sub); + }; + Dep.prototype.depend = function depend() { + if (Dep.target) { + Dep.target.addDep(this); + } + }; + Dep.prototype.notify = function notify() { + var subs = this.subs.slice(); + if (!config.async) { + subs.sort(function(a, b) { + return a.id - b.id; + }); + } + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } + }; + Dep.target = null; + var targetStack = []; + function pushTarget(target2) { + targetStack.push(target2); + Dep.target = target2; + } + function popTarget() { + targetStack.pop(); + Dep.target = targetStack[targetStack.length - 1]; + } + var VNode = function VNode2(tag, data, children, text2, elm, context, componentOptions, asyncFactory) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text2; + this.elm = elm; + this.ns = void 0; + this.context = context; + this.fnContext = void 0; + this.fnOptions = void 0; + this.fnScopeId = void 0; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = void 0; + this.parent = void 0; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = void 0; + this.isAsyncPlaceholder = false; + }; + var prototypeAccessors = { child: { configurable: true } }; + prototypeAccessors.child.get = function() { + return this.componentInstance; + }; + Object.defineProperties(VNode.prototype, prototypeAccessors); + var createEmptyVNode = function(text2) { + if (text2 === void 0) + text2 = ""; + var node = new VNode(); + node.text = text2; + node.isComment = true; + return node; + }; + function createTextVNode(val) { + return new VNode(void 0, void 0, void 0, String(val)); + } + function cloneVNode(vnode) { + var cloned = new VNode(vnode.tag, vnode.data, vnode.children && vnode.children.slice(), vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.asyncMeta = vnode.asyncMeta; + cloned.isCloned = true; + return cloned; + } + var arrayProto = Array.prototype; + var arrayMethods = Object.create(arrayProto); + var methodsToPatch = [ + "push", + "pop", + "shift", + "unshift", + "splice", + "sort", + "reverse" + ]; + methodsToPatch.forEach(function(method) { + var original = arrayProto[method]; + def(arrayMethods, method, function mutator() { + var args = [], len2 = arguments.length; + while (len2--) + args[len2] = arguments[len2]; + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted2; + switch (method) { + case "push": + case "unshift": + inserted2 = args; + break; + case "splice": + inserted2 = args.slice(2); + break; + } + if (inserted2) { + ob.observeArray(inserted2); + } + ob.dep.notify(); + return result; + }); + }); + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + var shouldObserve = true; + function toggleObserving(value) { + shouldObserve = value; + } + var Observer = function Observer2(value) { + this.value = value; + this.dep = new Dep(); + this.vmCount = 0; + def(value, "__ob__", this); + if (Array.isArray(value)) { + if (hasProto) { + protoAugment(value, arrayMethods); + } else { + copyAugment(value, arrayMethods, arrayKeys); + } + this.observeArray(value); + } else { + this.walk(value); + } + }; + Observer.prototype.walk = function walk(obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + defineReactive$$1(obj, keys[i]); + } + }; + Observer.prototype.observeArray = function observeArray(items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } + }; + function protoAugment(target2, src) { + target2.__proto__ = src; + } + function copyAugment(target2, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target2, key, src[key]); + } + } + function observe(value, asRootData) { + if (!isObject(value) || value instanceof VNode) { + return; + } + var ob; + if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if (shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) { + ob = new Observer(value); + } + if (asRootData && ob) { + ob.vmCount++; + } + return ob; + } + function defineReactive$$1(obj, key, val, customSetter, shallow) { + var dep = new Dep(); + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return; + } + var getter = property && property.get; + var setter = property && property.set; + if ((!getter || setter) && arguments.length === 2) { + val = obj[key]; + } + var childOb = !shallow && observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter() { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + if (Array.isArray(value)) { + dependArray(value); + } + } + } + return value; + }, + set: function reactiveSetter(newVal) { + var value = getter ? getter.call(obj) : val; + if (newVal === value || newVal !== newVal && value !== value) { + return; + } + if (customSetter) { + customSetter(); + } + if (getter && !setter) { + return; + } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = !shallow && observe(newVal); + dep.notify(); + } + }); + } + function set(target2, key, val) { + if (isUndef(target2) || isPrimitive(target2)) { + warn("Cannot set reactive property on undefined, null, or primitive value: " + target2); + } + if (Array.isArray(target2) && isValidArrayIndex(key)) { + target2.length = Math.max(target2.length, key); + target2.splice(key, 1, val); + return val; + } + if (key in target2 && !(key in Object.prototype)) { + target2[key] = val; + return val; + } + var ob = target2.__ob__; + if (target2._isVue || ob && ob.vmCount) { + warn("Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option."); + return val; + } + if (!ob) { + target2[key] = val; + return val; + } + defineReactive$$1(ob.value, key, val); + ob.dep.notify(); + return val; + } + function del(target2, key) { + if (isUndef(target2) || isPrimitive(target2)) { + warn("Cannot delete reactive property on undefined, null, or primitive value: " + target2); + } + if (Array.isArray(target2) && isValidArrayIndex(key)) { + target2.splice(key, 1); + return; + } + var ob = target2.__ob__; + if (target2._isVue || ob && ob.vmCount) { + warn("Avoid deleting properties on a Vue instance or its root $data - just set it to null."); + return; + } + if (!hasOwn(target2, key)) { + return; + } + delete target2[key]; + if (!ob) { + return; + } + ob.dep.notify(); + } + function dependArray(value) { + for (var e = void 0, i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + if (Array.isArray(e)) { + dependArray(e); + } + } + } + var strats = config.optionMergeStrategies; + if (true) { + strats.el = strats.propsData = function(parent, child, vm, key) { + if (!vm) { + warn('option "' + key + '" can only be used during instance creation with the `new` keyword.'); + } + return defaultStrat(parent, child); + }; + } + function mergeData(to, from) { + if (!from) { + return to; + } + var key, toVal, fromVal; + var keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from); + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + if (key === "__ob__") { + continue; + } + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if (toVal !== fromVal && isPlainObject(toVal) && isPlainObject(fromVal)) { + mergeData(toVal, fromVal); + } + } + return to; + } + function mergeDataOrFn(parentVal, childVal, vm) { + if (!vm) { + if (!childVal) { + return parentVal; + } + if (!parentVal) { + return childVal; + } + return function mergedDataFn() { + return mergeData(typeof childVal === "function" ? childVal.call(this, this) : childVal, typeof parentVal === "function" ? parentVal.call(this, this) : parentVal); + }; + } else { + return function mergedInstanceDataFn() { + var instanceData = typeof childVal === "function" ? childVal.call(vm, vm) : childVal; + var defaultData = typeof parentVal === "function" ? parentVal.call(vm, vm) : parentVal; + if (instanceData) { + return mergeData(instanceData, defaultData); + } else { + return defaultData; + } + }; + } + } + strats.data = function(parentVal, childVal, vm) { + if (!vm) { + if (childVal && typeof childVal !== "function") { + warn('The "data" option should be a function that returns a per-instance value in component definitions.', vm); + return parentVal; + } + return mergeDataOrFn(parentVal, childVal); + } + return mergeDataOrFn(parentVal, childVal, vm); + }; + function mergeHook(parentVal, childVal) { + var res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal; + return res ? dedupeHooks(res) : res; + } + function dedupeHooks(hooks2) { + var res = []; + for (var i = 0; i < hooks2.length; i++) { + if (res.indexOf(hooks2[i]) === -1) { + res.push(hooks2[i]); + } + } + return res; + } + LIFECYCLE_HOOKS.forEach(function(hook) { + strats[hook] = mergeHook; + }); + function mergeAssets(parentVal, childVal, vm, key) { + var res = Object.create(parentVal || null); + if (childVal) { + assertObjectType(key, childVal, vm); + return extend(res, childVal); + } else { + return res; + } + } + ASSET_TYPES.forEach(function(type) { + strats[type + "s"] = mergeAssets; + }); + strats.watch = function(parentVal, childVal, vm, key) { + if (parentVal === nativeWatch) { + parentVal = void 0; + } + if (childVal === nativeWatch) { + childVal = void 0; + } + if (!childVal) { + return Object.create(parentVal || null); + } + if (true) { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { + return childVal; + } + var ret = {}; + extend(ret, parentVal); + for (var key$1 in childVal) { + var parent = ret[key$1]; + var child = childVal[key$1]; + if (parent && !Array.isArray(parent)) { + parent = [parent]; + } + ret[key$1] = parent ? parent.concat(child) : Array.isArray(child) ? child : [child]; + } + return ret; + }; + strats.props = strats.methods = strats.inject = strats.computed = function(parentVal, childVal, vm, key) { + if (childVal && true) { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { + return childVal; + } + var ret = /* @__PURE__ */ Object.create(null); + extend(ret, parentVal); + if (childVal) { + extend(ret, childVal); + } + return ret; + }; + strats.provide = mergeDataOrFn; + var defaultStrat = function(parentVal, childVal) { + return childVal === void 0 ? parentVal : childVal; + }; + function checkComponents(options) { + for (var key in options.components) { + validateComponentName(key); + } + } + function validateComponentName(name) { + if (!new RegExp("^[a-zA-Z][\\-\\.0-9_" + unicodeRegExp.source + "]*$").test(name)) { + warn('Invalid component name: "' + name + '". Component names should conform to valid custom element name in html5 specification.'); + } + if (isBuiltInTag(name) || config.isReservedTag(name)) { + warn("Do not use built-in or reserved HTML elements as component id: " + name); + } + } + function normalizeProps(options, vm) { + var props2 = options.props; + if (!props2) { + return; + } + var res = {}; + var i, val, name; + if (Array.isArray(props2)) { + i = props2.length; + while (i--) { + val = props2[i]; + if (typeof val === "string") { + name = camelize(val); + res[name] = { type: null }; + } else if (true) { + warn("props must be strings when using array syntax."); + } + } + } else if (isPlainObject(props2)) { + for (var key in props2) { + val = props2[key]; + name = camelize(key); + res[name] = isPlainObject(val) ? val : { type: val }; + } + } else if (true) { + warn('Invalid value for option "props": expected an Array or an Object, but got ' + toRawType(props2) + ".", vm); + } + options.props = res; + } + function normalizeInject(options, vm) { + var inject = options.inject; + if (!inject) { + return; + } + var normalized = options.inject = {}; + if (Array.isArray(inject)) { + for (var i = 0; i < inject.length; i++) { + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + for (var key in inject) { + var val = inject[key]; + normalized[key] = isPlainObject(val) ? extend({ from: key }, val) : { from: val }; + } + } else if (true) { + warn('Invalid value for option "inject": expected an Array or an Object, but got ' + toRawType(inject) + ".", vm); + } + } + function normalizeDirectives(options) { + var dirs = options.directives; + if (dirs) { + for (var key in dirs) { + var def$$1 = dirs[key]; + if (typeof def$$1 === "function") { + dirs[key] = { bind: def$$1, update: def$$1 }; + } + } + } + } + function assertObjectType(name, value, vm) { + if (!isPlainObject(value)) { + warn('Invalid value for option "' + name + '": expected an Object, but got ' + toRawType(value) + ".", vm); + } + } + function mergeOptions(parent, child, vm) { + if (true) { + checkComponents(child); + } + if (typeof child === "function") { + child = child.options; + } + normalizeProps(child, vm); + normalizeInject(child, vm); + normalizeDirectives(child); + if (!child._base) { + if (child.extends) { + parent = mergeOptions(parent, child.extends, vm); + } + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + } + var options = {}; + var key; + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField(key2) { + var strat = strats[key2] || defaultStrat; + options[key2] = strat(parent[key2], child[key2], vm, key2); + } + return options; + } + function resolveAsset(options, type, id, warnMissing) { + if (typeof id !== "string") { + return; + } + var assets = options[type]; + if (hasOwn(assets, id)) { + return assets[id]; + } + var camelizedId = camelize(id); + if (hasOwn(assets, camelizedId)) { + return assets[camelizedId]; + } + var PascalCaseId = capitalize(camelizedId); + if (hasOwn(assets, PascalCaseId)) { + return assets[PascalCaseId]; + } + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + if (warnMissing && !res) { + warn("Failed to resolve " + type.slice(0, -1) + ": " + id, options); + } + return res; + } + function validateProp(key, propOptions, propsData, vm) { + var prop = propOptions[key]; + var absent = !hasOwn(propsData, key); + var value = propsData[key]; + var booleanIndex = getTypeIndex(Boolean, prop.type); + if (booleanIndex > -1) { + if (absent && !hasOwn(prop, "default")) { + value = false; + } else if (value === "" || value === hyphenate(key)) { + var stringIndex = getTypeIndex(String, prop.type); + if (stringIndex < 0 || booleanIndex < stringIndex) { + value = true; + } + } + } + if (value === void 0) { + value = getPropDefaultValue(vm, prop, key); + var prevShouldObserve = shouldObserve; + toggleObserving(true); + observe(value); + toggleObserving(prevShouldObserve); + } + if (true) { + assertProp(prop, key, value, vm, absent); + } + return value; + } + function getPropDefaultValue(vm, prop, key) { + if (!hasOwn(prop, "default")) { + return void 0; + } + var def2 = prop.default; + if (isObject(def2)) { + warn('Invalid default value for prop "' + key + '": Props with type Object/Array must use a factory function to return the default value.', vm); + } + if (vm && vm.$options.propsData && vm.$options.propsData[key] === void 0 && vm._props[key] !== void 0) { + return vm._props[key]; + } + return typeof def2 === "function" && getType(prop.type) !== "Function" ? def2.call(vm) : def2; + } + function assertProp(prop, name, value, vm, absent) { + if (prop.required && absent) { + warn('Missing required prop: "' + name + '"', vm); + return; + } + if (value == null && !prop.required) { + return; + } + var type = prop.type; + var valid = !type || type === true; + var expectedTypes = []; + if (type) { + if (!Array.isArray(type)) { + type = [type]; + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ""); + valid = assertedType.valid; + } + } + if (!valid) { + warn(getInvalidTypeMessage(name, value, expectedTypes), vm); + return; + } + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + warn('Invalid prop: custom validator check failed for prop "' + name + '".', vm); + } + } + } + var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + function assertType(value, type) { + var valid; + var expectedType = getType(type); + if (simpleCheckRE.test(expectedType)) { + var t = typeof value; + valid = t === expectedType.toLowerCase(); + if (!valid && t === "object") { + valid = value instanceof type; + } + } else if (expectedType === "Object") { + valid = isPlainObject(value); + } else if (expectedType === "Array") { + valid = Array.isArray(value); + } else { + valid = value instanceof type; + } + return { + valid, + expectedType + }; + } + function getType(fn) { + var match = fn && fn.toString().match(/^\s*function (\w+)/); + return match ? match[1] : ""; + } + function isSameType(a, b) { + return getType(a) === getType(b); + } + function getTypeIndex(type, expectedTypes) { + if (!Array.isArray(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1; + } + for (var i = 0, len2 = expectedTypes.length; i < len2; i++) { + if (isSameType(expectedTypes[i], type)) { + return i; + } + } + return -1; + } + function getInvalidTypeMessage(name, value, expectedTypes) { + var message = 'Invalid prop: type check failed for prop "' + name + '". Expected ' + expectedTypes.map(capitalize).join(", "); + var expectedType = expectedTypes[0]; + var receivedType = toRawType(value); + var expectedValue = styleValue(value, expectedType); + var receivedValue = styleValue(value, receivedType); + if (expectedTypes.length === 1 && isExplicable(expectedType) && !isBoolean(expectedType, receivedType)) { + message += " with value " + expectedValue; + } + message += ", got " + receivedType + " "; + if (isExplicable(receivedType)) { + message += "with value " + receivedValue + "."; + } + return message; + } + function styleValue(value, type) { + if (type === "String") { + return '"' + value + '"'; + } else if (type === "Number") { + return "" + Number(value); + } else { + return "" + value; + } + } + function isExplicable(value) { + var explicitTypes = ["string", "number", "boolean"]; + return explicitTypes.some(function(elem) { + return value.toLowerCase() === elem; + }); + } + function isBoolean() { + var args = [], len2 = arguments.length; + while (len2--) + args[len2] = arguments[len2]; + return args.some(function(elem) { + return elem.toLowerCase() === "boolean"; + }); + } + function handleError(err, vm, info) { + pushTarget(); + try { + if (vm) { + var cur = vm; + while (cur = cur.$parent) { + var hooks2 = cur.$options.errorCaptured; + if (hooks2) { + for (var i = 0; i < hooks2.length; i++) { + try { + var capture = hooks2[i].call(cur, err, vm, info) === false; + if (capture) { + return; + } + } catch (e) { + globalHandleError(e, cur, "errorCaptured hook"); + } + } + } + } + } + globalHandleError(err, vm, info); + } finally { + popTarget(); + } + } + function invokeWithErrorHandling(handler, context, args, vm, info) { + var res; + try { + res = args ? handler.apply(context, args) : handler.call(context); + if (res && !res._isVue && isPromise(res) && !res._handled) { + res.catch(function(e) { + return handleError(e, vm, info + " (Promise/async)"); + }); + res._handled = true; + } + } catch (e) { + handleError(e, vm, info); + } + return res; + } + function globalHandleError(err, vm, info) { + if (config.errorHandler) { + try { + return config.errorHandler.call(null, err, vm, info); + } catch (e) { + if (e !== err) { + logError(e, null, "config.errorHandler"); + } + } + } + logError(err, vm, info); + } + function logError(err, vm, info) { + if (true) { + warn("Error in " + info + ': "' + err.toString() + '"', vm); + } + if ((inBrowser || inWeex) && typeof console !== "undefined") { + console.error(err); + } else { + throw err; + } + } + var isUsingMicroTask = false; + var callbacks = []; + var pending = false; + function flushCallbacks() { + pending = false; + var copies = callbacks.slice(0); + callbacks.length = 0; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } + } + var timerFunc; + if (typeof Promise !== "undefined" && isNative(Promise)) { + p = Promise.resolve(); + timerFunc = function() { + p.then(flushCallbacks); + if (isIOS) { + setTimeout(noop); + } + }; + isUsingMicroTask = true; + } else if (!isIE && typeof MutationObserver !== "undefined" && (isNative(MutationObserver) || MutationObserver.toString() === "[object MutationObserverConstructor]")) { + counter = 1; + observer = new MutationObserver(flushCallbacks); + textNode = document.createTextNode(String(counter)); + observer.observe(textNode, { + characterData: true + }); + timerFunc = function() { + counter = (counter + 1) % 2; + textNode.data = String(counter); + }; + isUsingMicroTask = true; + } else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) { + timerFunc = function() { + setImmediate(flushCallbacks); + }; + } else { + timerFunc = function() { + setTimeout(flushCallbacks, 0); + }; + } + var p; + var counter; + var observer; + var textNode; + function nextTick(cb, ctx) { + var _resolve; + callbacks.push(function() { + if (cb) { + try { + cb.call(ctx); + } catch (e) { + handleError(e, ctx, "nextTick"); + } + } else if (_resolve) { + _resolve(ctx); + } + }); + if (!pending) { + pending = true; + timerFunc(); + } + if (!cb && typeof Promise !== "undefined") { + return new Promise(function(resolve) { + _resolve = resolve; + }); + } + } + var mark; + var measure; + if (true) { + perf = inBrowser && window.performance; + if (perf && perf.mark && perf.measure && perf.clearMarks && perf.clearMeasures) { + mark = function(tag) { + return perf.mark(tag); + }; + measure = function(name, startTag, endTag2) { + perf.measure(name, startTag, endTag2); + perf.clearMarks(startTag); + perf.clearMarks(endTag2); + }; + } + } + var perf; + var initProxy; + if (true) { + allowedGlobals = makeMap("Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,require"); + warnNonPresent = function(target2, key) { + warn('Property or method "' + key + '" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', target2); + }; + warnReservedPrefix = function(target2, key) { + warn('Property "' + key + '" must be accessed with "$data.' + key + '" because properties starting with "$" or "_" are not proxied in the Vue instance to prevent conflicts with Vue internals. See: https://vuejs.org/v2/api/#data', target2); + }; + hasProxy = typeof Proxy !== "undefined" && isNative(Proxy); + if (hasProxy) { + isBuiltInModifier = makeMap("stop,prevent,self,ctrl,shift,alt,meta,exact"); + config.keyCodes = new Proxy(config.keyCodes, { + set: function set2(target2, key, value) { + if (isBuiltInModifier(key)) { + warn("Avoid overwriting built-in modifier in config.keyCodes: ." + key); + return false; + } else { + target2[key] = value; + return true; + } + } + }); + } + hasHandler = { + has: function has2(target2, key) { + var has3 = key in target2; + var isAllowed = allowedGlobals(key) || typeof key === "string" && key.charAt(0) === "_" && !(key in target2.$data); + if (!has3 && !isAllowed) { + if (key in target2.$data) { + warnReservedPrefix(target2, key); + } else { + warnNonPresent(target2, key); + } + } + return has3 || !isAllowed; + } + }; + getHandler = { + get: function get3(target2, key) { + if (typeof key === "string" && !(key in target2)) { + if (key in target2.$data) { + warnReservedPrefix(target2, key); + } else { + warnNonPresent(target2, key); + } + } + return target2[key]; + } + }; + initProxy = function initProxy2(vm) { + if (hasProxy) { + var options = vm.$options; + var handlers = options.render && options.render._withStripped ? getHandler : hasHandler; + vm._renderProxy = new Proxy(vm, handlers); + } else { + vm._renderProxy = vm; + } + }; + } + var allowedGlobals; + var warnNonPresent; + var warnReservedPrefix; + var hasProxy; + var isBuiltInModifier; + var hasHandler; + var getHandler; + var seenObjects = new _Set(); + function traverse(val) { + _traverse(val, seenObjects); + seenObjects.clear(); + } + function _traverse(val, seen) { + var i, keys; + var isA = Array.isArray(val); + if (!isA && !isObject(val) || Object.isFrozen(val) || val instanceof VNode) { + return; + } + if (val.__ob__) { + var depId = val.__ob__.dep.id; + if (seen.has(depId)) { + return; + } + seen.add(depId); + } + if (isA) { + i = val.length; + while (i--) { + _traverse(val[i], seen); + } + } else { + keys = Object.keys(val); + i = keys.length; + while (i--) { + _traverse(val[keys[i]], seen); + } + } + } + var normalizeEvent = cached(function(name) { + var passive = name.charAt(0) === "&"; + name = passive ? name.slice(1) : name; + var once$$1 = name.charAt(0) === "~"; + name = once$$1 ? name.slice(1) : name; + var capture = name.charAt(0) === "!"; + name = capture ? name.slice(1) : name; + return { + name, + once: once$$1, + capture, + passive + }; + }); + function createFnInvoker(fns, vm) { + function invoker() { + var arguments$1 = arguments; + var fns2 = invoker.fns; + if (Array.isArray(fns2)) { + var cloned = fns2.slice(); + for (var i = 0; i < cloned.length; i++) { + invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler"); + } + } else { + return invokeWithErrorHandling(fns2, null, arguments, vm, "v-on handler"); + } + } + invoker.fns = fns; + return invoker; + } + function updateListeners(on2, oldOn, add2, remove$$12, createOnceHandler2, vm) { + var name, def$$1, cur, old, event; + for (name in on2) { + def$$1 = cur = on2[name]; + old = oldOn[name]; + event = normalizeEvent(name); + if (isUndef(cur)) { + warn('Invalid handler for event "' + event.name + '": got ' + String(cur), vm); + } else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on2[name] = createFnInvoker(cur, vm); + } + if (isTrue(event.once)) { + cur = on2[name] = createOnceHandler2(event.name, cur, event.capture); + } + add2(event.name, cur, event.capture, event.passive, event.params); + } else if (cur !== old) { + old.fns = cur; + on2[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on2[name])) { + event = normalizeEvent(name); + remove$$12(event.name, oldOn[name], event.capture); + } + } + } + function mergeVNodeHook(def2, hookKey, hook) { + if (def2 instanceof VNode) { + def2 = def2.data.hook || (def2.data.hook = {}); + } + var invoker; + var oldHook = def2[hookKey]; + function wrappedHook() { + hook.apply(this, arguments); + remove(invoker.fns, wrappedHook); + } + if (isUndef(oldHook)) { + invoker = createFnInvoker([wrappedHook]); + } else { + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + invoker = oldHook; + invoker.fns.push(wrappedHook); + } else { + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + invoker.merged = true; + def2[hookKey] = invoker; + } + function extractPropsFromVNodeData(data, Ctor, tag) { + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return; + } + var res = {}; + var attrs2 = data.attrs; + var props2 = data.props; + if (isDef(attrs2) || isDef(props2)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + if (true) { + var keyInLowerCase = key.toLowerCase(); + if (key !== keyInLowerCase && attrs2 && hasOwn(attrs2, keyInLowerCase)) { + tip('Prop "' + keyInLowerCase + '" is passed to component ' + formatComponentName(tag || Ctor) + ', but the declared prop name is "' + key + '". Note that HTML attributes are case-insensitive and camelCased props need to use their kebab-case equivalents when using in-DOM templates. You should probably use "' + altKey + '" instead of "' + key + '".'); + } + } + checkProp(res, props2, key, altKey, true) || checkProp(res, attrs2, key, altKey, false); + } + } + return res; + } + function checkProp(res, hash2, key, altKey, preserve) { + if (isDef(hash2)) { + if (hasOwn(hash2, key)) { + res[key] = hash2[key]; + if (!preserve) { + delete hash2[key]; + } + return true; + } else if (hasOwn(hash2, altKey)) { + res[key] = hash2[altKey]; + if (!preserve) { + delete hash2[altKey]; + } + return true; + } + } + return false; + } + function simpleNormalizeChildren(children) { + for (var i = 0; i < children.length; i++) { + if (Array.isArray(children[i])) { + return Array.prototype.concat.apply([], children); + } + } + return children; + } + function normalizeChildren(children) { + return isPrimitive(children) ? [createTextVNode(children)] : Array.isArray(children) ? normalizeArrayChildren(children) : void 0; + } + function isTextNode(node) { + return isDef(node) && isDef(node.text) && isFalse(node.isComment); + } + function normalizeArrayChildren(children, nestedIndex) { + var res = []; + var i, c, lastIndex, last; + for (i = 0; i < children.length; i++) { + c = children[i]; + if (isUndef(c) || typeof c === "boolean") { + continue; + } + lastIndex = res.length - 1; + last = res[lastIndex]; + if (Array.isArray(c)) { + if (c.length > 0) { + c = normalizeArrayChildren(c, (nestedIndex || "") + "_" + i); + if (isTextNode(c[0]) && isTextNode(last)) { + res[lastIndex] = createTextVNode(last.text + c[0].text); + c.shift(); + } + res.push.apply(res, c); + } + } else if (isPrimitive(c)) { + if (isTextNode(last)) { + res[lastIndex] = createTextVNode(last.text + c); + } else if (c !== "") { + res.push(createTextVNode(c)); + } + } else { + if (isTextNode(c) && isTextNode(last)) { + res[lastIndex] = createTextVNode(last.text + c.text); + } else { + if (isTrue(children._isVList) && isDef(c.tag) && isUndef(c.key) && isDef(nestedIndex)) { + c.key = "__vlist" + nestedIndex + "_" + i + "__"; + } + res.push(c); + } + } + } + return res; + } + function initProvide(vm) { + var provide = vm.$options.provide; + if (provide) { + vm._provided = typeof provide === "function" ? provide.call(vm) : provide; + } + } + function initInjections(vm) { + var result = resolveInject(vm.$options.inject, vm); + if (result) { + toggleObserving(false); + Object.keys(result).forEach(function(key) { + if (true) { + defineReactive$$1(vm, key, result[key], function() { + warn('Avoid mutating an injected value directly since the changes will be overwritten whenever the provided component re-renders. injection being mutated: "' + key + '"', vm); + }); + } else { + defineReactive$$1(vm, key, result[key]); + } + }); + toggleObserving(true); + } + } + function resolveInject(inject, vm) { + if (inject) { + var result = /* @__PURE__ */ Object.create(null); + var keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (key === "__ob__") { + continue; + } + var provideKey = inject[key].from; + var source = vm; + while (source) { + if (source._provided && hasOwn(source._provided, provideKey)) { + result[key] = source._provided[provideKey]; + break; + } + source = source.$parent; + } + if (!source) { + if ("default" in inject[key]) { + var provideDefault = inject[key].default; + result[key] = typeof provideDefault === "function" ? provideDefault.call(vm) : provideDefault; + } else if (true) { + warn('Injection "' + key + '" not found', vm); + } + } + } + return result; + } + } + function resolveSlots(children, context) { + if (!children || !children.length) { + return {}; + } + var slots = {}; + for (var i = 0, l = children.length; i < l; i++) { + var child = children[i]; + var data = child.data; + if (data && data.attrs && data.attrs.slot) { + delete data.attrs.slot; + } + if ((child.context === context || child.fnContext === context) && data && data.slot != null) { + var name = data.slot; + var slot = slots[name] || (slots[name] = []); + if (child.tag === "template") { + slot.push.apply(slot, child.children || []); + } else { + slot.push(child); + } + } else { + (slots.default || (slots.default = [])).push(child); + } + } + for (var name$1 in slots) { + if (slots[name$1].every(isWhitespace)) { + delete slots[name$1]; + } + } + return slots; + } + function isWhitespace(node) { + return node.isComment && !node.asyncFactory || node.text === " "; + } + function normalizeScopedSlots(slots, normalSlots, prevSlots) { + var res; + var hasNormalSlots = Object.keys(normalSlots).length > 0; + var isStable = slots ? !!slots.$stable : !hasNormalSlots; + var key = slots && slots.$key; + if (!slots) { + res = {}; + } else if (slots._normalized) { + return slots._normalized; + } else if (isStable && prevSlots && prevSlots !== emptyObject && key === prevSlots.$key && !hasNormalSlots && !prevSlots.$hasNormal) { + return prevSlots; + } else { + res = {}; + for (var key$1 in slots) { + if (slots[key$1] && key$1[0] !== "$") { + res[key$1] = normalizeScopedSlot(normalSlots, key$1, slots[key$1]); + } + } + } + for (var key$2 in normalSlots) { + if (!(key$2 in res)) { + res[key$2] = proxyNormalSlot(normalSlots, key$2); + } + } + if (slots && Object.isExtensible(slots)) { + slots._normalized = res; + } + def(res, "$stable", isStable); + def(res, "$key", key); + def(res, "$hasNormal", hasNormalSlots); + return res; + } + function normalizeScopedSlot(normalSlots, key, fn) { + var normalized = function() { + var res = arguments.length ? fn.apply(null, arguments) : fn({}); + res = res && typeof res === "object" && !Array.isArray(res) ? [res] : normalizeChildren(res); + return res && (res.length === 0 || res.length === 1 && res[0].isComment) ? void 0 : res; + }; + if (fn.proxy) { + Object.defineProperty(normalSlots, key, { + get: normalized, + enumerable: true, + configurable: true + }); + } + return normalized; + } + function proxyNormalSlot(slots, key) { + return function() { + return slots[key]; + }; + } + function renderList(val, render4) { + var ret, i, l, keys, key; + if (Array.isArray(val) || typeof val === "string") { + ret = new Array(val.length); + for (i = 0, l = val.length; i < l; i++) { + ret[i] = render4(val[i], i); + } + } else if (typeof val === "number") { + ret = new Array(val); + for (i = 0; i < val; i++) { + ret[i] = render4(i + 1, i); + } + } else if (isObject(val)) { + if (hasSymbol && val[Symbol.iterator]) { + ret = []; + var iterator = val[Symbol.iterator](); + var result = iterator.next(); + while (!result.done) { + ret.push(render4(result.value, ret.length)); + result = iterator.next(); + } + } else { + keys = Object.keys(val); + ret = new Array(keys.length); + for (i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + ret[i] = render4(val[key], key, i); + } + } + } + if (!isDef(ret)) { + ret = []; + } + ret._isVList = true; + return ret; + } + function renderSlot(name, fallback, props2, bindObject) { + var scopedSlotFn = this.$scopedSlots[name]; + var nodes; + if (scopedSlotFn) { + props2 = props2 || {}; + if (bindObject) { + if (!isObject(bindObject)) { + warn("slot v-bind without argument expects an Object", this); + } + props2 = extend(extend({}, bindObject), props2); + } + nodes = scopedSlotFn(props2) || fallback; + } else { + nodes = this.$slots[name] || fallback; + } + var target2 = props2 && props2.slot; + if (target2) { + return this.$createElement("template", { slot: target2 }, nodes); + } else { + return nodes; + } + } + function resolveFilter(id) { + return resolveAsset(this.$options, "filters", id, true) || identity; + } + function isKeyNotMatch(expect, actual) { + if (Array.isArray(expect)) { + return expect.indexOf(actual) === -1; + } else { + return expect !== actual; + } + } + function checkKeyCodes(eventKeyCode, key, builtInKeyCode, eventKeyName, builtInKeyName) { + var mappedKeyCode = config.keyCodes[key] || builtInKeyCode; + if (builtInKeyName && eventKeyName && !config.keyCodes[key]) { + return isKeyNotMatch(builtInKeyName, eventKeyName); + } else if (mappedKeyCode) { + return isKeyNotMatch(mappedKeyCode, eventKeyCode); + } else if (eventKeyName) { + return hyphenate(eventKeyName) !== key; + } + } + function bindObjectProps(data, tag, value, asProp, isSync) { + if (value) { + if (!isObject(value)) { + warn("v-bind without argument expects an Object or Array value", this); + } else { + if (Array.isArray(value)) { + value = toObject(value); + } + var hash2; + var loop = function(key2) { + if (key2 === "class" || key2 === "style" || isReservedAttribute(key2)) { + hash2 = data; + } else { + var type = data.attrs && data.attrs.type; + hash2 = asProp || config.mustUseProp(tag, type, key2) ? data.domProps || (data.domProps = {}) : data.attrs || (data.attrs = {}); + } + var camelizedKey = camelize(key2); + var hyphenatedKey = hyphenate(key2); + if (!(camelizedKey in hash2) && !(hyphenatedKey in hash2)) { + hash2[key2] = value[key2]; + if (isSync) { + var on2 = data.on || (data.on = {}); + on2["update:" + key2] = function($event) { + value[key2] = $event; + }; + } + } + }; + for (var key in value) + loop(key); + } + } + return data; + } + function renderStatic(index2, isInFor) { + var cached2 = this._staticTrees || (this._staticTrees = []); + var tree = cached2[index2]; + if (tree && !isInFor) { + return tree; + } + tree = cached2[index2] = this.$options.staticRenderFns[index2].call(this._renderProxy, null, this); + markStatic(tree, "__static__" + index2, false); + return tree; + } + function markOnce(tree, index2, key) { + markStatic(tree, "__once__" + index2 + (key ? "_" + key : ""), true); + return tree; + } + function markStatic(tree, key, isOnce) { + if (Array.isArray(tree)) { + for (var i = 0; i < tree.length; i++) { + if (tree[i] && typeof tree[i] !== "string") { + markStaticNode(tree[i], key + "_" + i, isOnce); + } + } + } else { + markStaticNode(tree, key, isOnce); + } + } + function markStaticNode(node, key, isOnce) { + node.isStatic = true; + node.key = key; + node.isOnce = isOnce; + } + function bindObjectListeners(data, value) { + if (value) { + if (!isPlainObject(value)) { + warn("v-on without argument expects an Object value", this); + } else { + var on2 = data.on = data.on ? extend({}, data.on) : {}; + for (var key in value) { + var existing = on2[key]; + var ours = value[key]; + on2[key] = existing ? [].concat(existing, ours) : ours; + } + } + } + return data; + } + function resolveScopedSlots(fns, res, hasDynamicKeys, contentHashKey) { + res = res || { $stable: !hasDynamicKeys }; + for (var i = 0; i < fns.length; i++) { + var slot = fns[i]; + if (Array.isArray(slot)) { + resolveScopedSlots(slot, res, hasDynamicKeys); + } else if (slot) { + if (slot.proxy) { + slot.fn.proxy = true; + } + res[slot.key] = slot.fn; + } + } + if (contentHashKey) { + res.$key = contentHashKey; + } + return res; + } + function bindDynamicKeys(baseObj, values) { + for (var i = 0; i < values.length; i += 2) { + var key = values[i]; + if (typeof key === "string" && key) { + baseObj[values[i]] = values[i + 1]; + } else if (key !== "" && key !== null) { + warn("Invalid value for dynamic directive argument (expected string or null): " + key, this); + } + } + return baseObj; + } + function prependModifier(value, symbol) { + return typeof value === "string" ? symbol + value : value; + } + function installRenderHelpers(target2) { + target2._o = markOnce; + target2._n = toNumber; + target2._s = toString; + target2._l = renderList; + target2._t = renderSlot; + target2._q = looseEqual; + target2._i = looseIndexOf; + target2._m = renderStatic; + target2._f = resolveFilter; + target2._k = checkKeyCodes; + target2._b = bindObjectProps; + target2._v = createTextVNode; + target2._e = createEmptyVNode; + target2._u = resolveScopedSlots; + target2._g = bindObjectListeners; + target2._d = bindDynamicKeys; + target2._p = prependModifier; + } + function FunctionalRenderContext(data, props2, children, parent, Ctor) { + var this$1 = this; + var options = Ctor.options; + var contextVm; + if (hasOwn(parent, "_uid")) { + contextVm = Object.create(parent); + contextVm._original = parent; + } else { + contextVm = parent; + parent = parent._original; + } + var isCompiled = isTrue(options._compiled); + var needNormalization = !isCompiled; + this.data = data; + this.props = props2; + this.children = children; + this.parent = parent; + this.listeners = data.on || emptyObject; + this.injections = resolveInject(options.inject, parent); + this.slots = function() { + if (!this$1.$slots) { + normalizeScopedSlots(data.scopedSlots, this$1.$slots = resolveSlots(children, parent)); + } + return this$1.$slots; + }; + Object.defineProperty(this, "scopedSlots", { + enumerable: true, + get: function get3() { + return normalizeScopedSlots(data.scopedSlots, this.slots()); + } + }); + if (isCompiled) { + this.$options = options; + this.$slots = this.slots(); + this.$scopedSlots = normalizeScopedSlots(data.scopedSlots, this.$slots); + } + if (options._scopeId) { + this._c = function(a, b, c, d) { + var vnode = createElement(contextVm, a, b, c, d, needNormalization); + if (vnode && !Array.isArray(vnode)) { + vnode.fnScopeId = options._scopeId; + vnode.fnContext = parent; + } + return vnode; + }; + } else { + this._c = function(a, b, c, d) { + return createElement(contextVm, a, b, c, d, needNormalization); + }; + } + } + installRenderHelpers(FunctionalRenderContext.prototype); + function createFunctionalComponent(Ctor, propsData, data, contextVm, children) { + var options = Ctor.options; + var props2 = {}; + var propOptions = options.props; + if (isDef(propOptions)) { + for (var key in propOptions) { + props2[key] = validateProp(key, propOptions, propsData || emptyObject); + } + } else { + if (isDef(data.attrs)) { + mergeProps(props2, data.attrs); + } + if (isDef(data.props)) { + mergeProps(props2, data.props); + } + } + var renderContext = new FunctionalRenderContext(data, props2, children, contextVm, Ctor); + var vnode = options.render.call(null, renderContext._c, renderContext); + if (vnode instanceof VNode) { + return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext); + } else if (Array.isArray(vnode)) { + var vnodes = normalizeChildren(vnode) || []; + var res = new Array(vnodes.length); + for (var i = 0; i < vnodes.length; i++) { + res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext); + } + return res; + } + } + function cloneAndMarkFunctionalResult(vnode, data, contextVm, options, renderContext) { + var clone = cloneVNode(vnode); + clone.fnContext = contextVm; + clone.fnOptions = options; + if (true) { + (clone.devtoolsMeta = clone.devtoolsMeta || {}).renderContext = renderContext; + } + if (data.slot) { + (clone.data || (clone.data = {})).slot = data.slot; + } + return clone; + } + function mergeProps(to, from) { + for (var key in from) { + to[camelize(key)] = from[key]; + } + } + var componentVNodeHooks = { + init: function init(vnode, hydrating) { + if (vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive) { + var mountedNode = vnode; + componentVNodeHooks.prepatch(mountedNode, mountedNode); + } else { + var child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance); + child.$mount(hydrating ? vnode.elm : void 0, hydrating); + } + }, + prepatch: function prepatch(oldVnode, vnode) { + var options = vnode.componentOptions; + var child = vnode.componentInstance = oldVnode.componentInstance; + updateChildComponent(child, options.propsData, options.listeners, vnode, options.children); + }, + insert: function insert(vnode) { + var context = vnode.context; + var componentInstance = vnode.componentInstance; + if (!componentInstance._isMounted) { + componentInstance._isMounted = true; + callHook(componentInstance, "mounted"); + } + if (vnode.data.keepAlive) { + if (context._isMounted) { + queueActivatedComponent(componentInstance); + } else { + activateChildComponent(componentInstance, true); + } + } + }, + destroy: function destroy(vnode) { + var componentInstance = vnode.componentInstance; + if (!componentInstance._isDestroyed) { + if (!vnode.data.keepAlive) { + componentInstance.$destroy(); + } else { + deactivateChildComponent(componentInstance, true); + } + } + } + }; + var hooksToMerge = Object.keys(componentVNodeHooks); + function createComponent(Ctor, data, context, children, tag) { + if (isUndef(Ctor)) { + return; + } + var baseCtor = context.$options._base; + if (isObject(Ctor)) { + Ctor = baseCtor.extend(Ctor); + } + if (typeof Ctor !== "function") { + if (true) { + warn("Invalid Component definition: " + String(Ctor), context); + } + return; + } + var asyncFactory; + if (isUndef(Ctor.cid)) { + asyncFactory = Ctor; + Ctor = resolveAsyncComponent(asyncFactory, baseCtor); + if (Ctor === void 0) { + return createAsyncPlaceholder(asyncFactory, data, context, children, tag); + } + } + data = data || {}; + resolveConstructorOptions(Ctor); + if (isDef(data.model)) { + transformModel(Ctor.options, data); + } + var propsData = extractPropsFromVNodeData(data, Ctor, tag); + if (isTrue(Ctor.options.functional)) { + return createFunctionalComponent(Ctor, propsData, data, context, children); + } + var listeners = data.on; + data.on = data.nativeOn; + if (isTrue(Ctor.options.abstract)) { + var slot = data.slot; + data = {}; + if (slot) { + data.slot = slot; + } + } + installComponentHooks(data); + var name = Ctor.options.name || tag; + var vnode = new VNode("vue-component-" + Ctor.cid + (name ? "-" + name : ""), data, void 0, void 0, void 0, context, { Ctor, propsData, listeners, tag, children }, asyncFactory); + return vnode; + } + function createComponentInstanceForVnode(vnode, parent) { + var options = { + _isComponent: true, + _parentVnode: vnode, + parent + }; + var inlineTemplate = vnode.data.inlineTemplate; + if (isDef(inlineTemplate)) { + options.render = inlineTemplate.render; + options.staticRenderFns = inlineTemplate.staticRenderFns; + } + return new vnode.componentOptions.Ctor(options); + } + function installComponentHooks(data) { + var hooks2 = data.hook || (data.hook = {}); + for (var i = 0; i < hooksToMerge.length; i++) { + var key = hooksToMerge[i]; + var existing = hooks2[key]; + var toMerge = componentVNodeHooks[key]; + if (existing !== toMerge && !(existing && existing._merged)) { + hooks2[key] = existing ? mergeHook$1(toMerge, existing) : toMerge; + } + } + } + function mergeHook$1(f1, f2) { + var merged = function(a, b) { + f1(a, b); + f2(a, b); + }; + merged._merged = true; + return merged; + } + function transformModel(options, data) { + var prop = options.model && options.model.prop || "value"; + var event = options.model && options.model.event || "input"; + (data.attrs || (data.attrs = {}))[prop] = data.model.value; + var on2 = data.on || (data.on = {}); + var existing = on2[event]; + var callback = data.model.callback; + if (isDef(existing)) { + if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback) { + on2[event] = [callback].concat(existing); + } + } else { + on2[event] = callback; + } + } + var SIMPLE_NORMALIZE = 1; + var ALWAYS_NORMALIZE = 2; + function createElement(context, tag, data, children, normalizationType, alwaysNormalize) { + if (Array.isArray(data) || isPrimitive(data)) { + normalizationType = children; + children = data; + data = void 0; + } + if (isTrue(alwaysNormalize)) { + normalizationType = ALWAYS_NORMALIZE; + } + return _createElement(context, tag, data, children, normalizationType); + } + function _createElement(context, tag, data, children, normalizationType) { + if (isDef(data) && isDef(data.__ob__)) { + warn("Avoid using observed data object as vnode data: " + JSON.stringify(data) + "\nAlways create fresh vnode data objects in each render!", context); + return createEmptyVNode(); + } + if (isDef(data) && isDef(data.is)) { + tag = data.is; + } + if (!tag) { + return createEmptyVNode(); + } + if (isDef(data) && isDef(data.key) && !isPrimitive(data.key)) { + { + warn("Avoid using non-primitive value as key, use string/number value instead.", context); + } + } + if (Array.isArray(children) && typeof children[0] === "function") { + data = data || {}; + data.scopedSlots = { default: children[0] }; + children.length = 0; + } + if (normalizationType === ALWAYS_NORMALIZE) { + children = normalizeChildren(children); + } else if (normalizationType === SIMPLE_NORMALIZE) { + children = simpleNormalizeChildren(children); + } + var vnode, ns; + if (typeof tag === "string") { + var Ctor; + ns = context.$vnode && context.$vnode.ns || config.getTagNamespace(tag); + if (config.isReservedTag(tag)) { + if (isDef(data) && isDef(data.nativeOn)) { + warn("The .native modifier for v-on is only valid on components but it was used on <" + tag + ">.", context); + } + vnode = new VNode(config.parsePlatformTagName(tag), data, children, void 0, void 0, context); + } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, "components", tag))) { + vnode = createComponent(Ctor, data, context, children, tag); + } else { + vnode = new VNode(tag, data, children, void 0, void 0, context); + } + } else { + vnode = createComponent(tag, data, context, children); + } + if (Array.isArray(vnode)) { + return vnode; + } else if (isDef(vnode)) { + if (isDef(ns)) { + applyNS(vnode, ns); + } + if (isDef(data)) { + registerDeepBindings(data); + } + return vnode; + } else { + return createEmptyVNode(); + } + } + function applyNS(vnode, ns, force) { + vnode.ns = ns; + if (vnode.tag === "foreignObject") { + ns = void 0; + force = true; + } + if (isDef(vnode.children)) { + for (var i = 0, l = vnode.children.length; i < l; i++) { + var child = vnode.children[i]; + if (isDef(child.tag) && (isUndef(child.ns) || isTrue(force) && child.tag !== "svg")) { + applyNS(child, ns, force); + } + } + } + } + function registerDeepBindings(data) { + if (isObject(data.style)) { + traverse(data.style); + } + if (isObject(data.class)) { + traverse(data.class); + } + } + function initRender(vm) { + vm._vnode = null; + vm._staticTrees = null; + var options = vm.$options; + var parentVnode = vm.$vnode = options._parentVnode; + var renderContext = parentVnode && parentVnode.context; + vm.$slots = resolveSlots(options._renderChildren, renderContext); + vm.$scopedSlots = emptyObject; + vm._c = function(a, b, c, d) { + return createElement(vm, a, b, c, d, false); + }; + vm.$createElement = function(a, b, c, d) { + return createElement(vm, a, b, c, d, true); + }; + var parentData = parentVnode && parentVnode.data; + if (true) { + defineReactive$$1(vm, "$attrs", parentData && parentData.attrs || emptyObject, function() { + !isUpdatingChildComponent && warn("$attrs is readonly.", vm); + }, true); + defineReactive$$1(vm, "$listeners", options._parentListeners || emptyObject, function() { + !isUpdatingChildComponent && warn("$listeners is readonly.", vm); + }, true); + } else { + defineReactive$$1(vm, "$attrs", parentData && parentData.attrs || emptyObject, null, true); + defineReactive$$1(vm, "$listeners", options._parentListeners || emptyObject, null, true); + } + } + var currentRenderingInstance = null; + function renderMixin(Vue2) { + installRenderHelpers(Vue2.prototype); + Vue2.prototype.$nextTick = function(fn) { + return nextTick(fn, this); + }; + Vue2.prototype._render = function() { + var vm = this; + var ref2 = vm.$options; + var render4 = ref2.render; + var _parentVnode = ref2._parentVnode; + if (_parentVnode) { + vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots); + } + vm.$vnode = _parentVnode; + var vnode; + try { + currentRenderingInstance = vm; + vnode = render4.call(vm._renderProxy, vm.$createElement); + } catch (e) { + handleError(e, vm, "render"); + if (vm.$options.renderError) { + try { + vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e); + } catch (e2) { + handleError(e2, vm, "renderError"); + vnode = vm._vnode; + } + } else { + vnode = vm._vnode; + } + } finally { + currentRenderingInstance = null; + } + if (Array.isArray(vnode) && vnode.length === 1) { + vnode = vnode[0]; + } + if (!(vnode instanceof VNode)) { + if (Array.isArray(vnode)) { + warn("Multiple root nodes returned from render function. Render function should return a single root node.", vm); + } + vnode = createEmptyVNode(); + } + vnode.parent = _parentVnode; + return vnode; + }; + } + function ensureCtor(comp, base) { + if (comp.__esModule || hasSymbol && comp[Symbol.toStringTag] === "Module") { + comp = comp.default; + } + return isObject(comp) ? base.extend(comp) : comp; + } + function createAsyncPlaceholder(factory, data, context, children, tag) { + var node = createEmptyVNode(); + node.asyncFactory = factory; + node.asyncMeta = { data, context, children, tag }; + return node; + } + function resolveAsyncComponent(factory, baseCtor) { + if (isTrue(factory.error) && isDef(factory.errorComp)) { + return factory.errorComp; + } + if (isDef(factory.resolved)) { + return factory.resolved; + } + var owner = currentRenderingInstance; + if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) { + factory.owners.push(owner); + } + if (isTrue(factory.loading) && isDef(factory.loadingComp)) { + return factory.loadingComp; + } + if (owner && !isDef(factory.owners)) { + var owners = factory.owners = [owner]; + var sync = true; + var timerLoading = null; + var timerTimeout = null; + owner.$on("hook:destroyed", function() { + return remove(owners, owner); + }); + var forceRender = function(renderCompleted) { + for (var i = 0, l = owners.length; i < l; i++) { + owners[i].$forceUpdate(); + } + if (renderCompleted) { + owners.length = 0; + if (timerLoading !== null) { + clearTimeout(timerLoading); + timerLoading = null; + } + if (timerTimeout !== null) { + clearTimeout(timerTimeout); + timerTimeout = null; + } + } + }; + var resolve = once(function(res2) { + factory.resolved = ensureCtor(res2, baseCtor); + if (!sync) { + forceRender(true); + } else { + owners.length = 0; + } + }); + var reject = once(function(reason) { + warn("Failed to resolve async component: " + String(factory) + (reason ? "\nReason: " + reason : "")); + if (isDef(factory.errorComp)) { + factory.error = true; + forceRender(true); + } + }); + var res = factory(resolve, reject); + if (isObject(res)) { + if (isPromise(res)) { + if (isUndef(factory.resolved)) { + res.then(resolve, reject); + } + } else if (isPromise(res.component)) { + res.component.then(resolve, reject); + if (isDef(res.error)) { + factory.errorComp = ensureCtor(res.error, baseCtor); + } + if (isDef(res.loading)) { + factory.loadingComp = ensureCtor(res.loading, baseCtor); + if (res.delay === 0) { + factory.loading = true; + } else { + timerLoading = setTimeout(function() { + timerLoading = null; + if (isUndef(factory.resolved) && isUndef(factory.error)) { + factory.loading = true; + forceRender(false); + } + }, res.delay || 200); + } + } + if (isDef(res.timeout)) { + timerTimeout = setTimeout(function() { + timerTimeout = null; + if (isUndef(factory.resolved)) { + reject(true ? "timeout (" + res.timeout + "ms)" : null); + } + }, res.timeout); + } + } + } + sync = false; + return factory.loading ? factory.loadingComp : factory.resolved; + } + } + function isAsyncPlaceholder(node) { + return node.isComment && node.asyncFactory; + } + function getFirstComponentChild(children) { + if (Array.isArray(children)) { + for (var i = 0; i < children.length; i++) { + var c = children[i]; + if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) { + return c; + } + } + } + } + function initEvents(vm) { + vm._events = /* @__PURE__ */ Object.create(null); + vm._hasHookEvent = false; + var listeners = vm.$options._parentListeners; + if (listeners) { + updateComponentListeners(vm, listeners); + } + } + var target; + function add(event, fn) { + target.$on(event, fn); + } + function remove$1(event, fn) { + target.$off(event, fn); + } + function createOnceHandler(event, fn) { + var _target = target; + return function onceHandler() { + var res = fn.apply(null, arguments); + if (res !== null) { + _target.$off(event, onceHandler); + } + }; + } + function updateComponentListeners(vm, listeners, oldListeners) { + target = vm; + updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm); + target = void 0; + } + function eventsMixin(Vue2) { + var hookRE = /^hook:/; + Vue2.prototype.$on = function(event, fn) { + var vm = this; + if (Array.isArray(event)) { + for (var i = 0, l = event.length; i < l; i++) { + vm.$on(event[i], fn); + } + } else { + (vm._events[event] || (vm._events[event] = [])).push(fn); + if (hookRE.test(event)) { + vm._hasHookEvent = true; + } + } + return vm; + }; + Vue2.prototype.$once = function(event, fn) { + var vm = this; + function on2() { + vm.$off(event, on2); + fn.apply(vm, arguments); + } + on2.fn = fn; + vm.$on(event, on2); + return vm; + }; + Vue2.prototype.$off = function(event, fn) { + var vm = this; + if (!arguments.length) { + vm._events = /* @__PURE__ */ Object.create(null); + return vm; + } + if (Array.isArray(event)) { + for (var i$1 = 0, l = event.length; i$1 < l; i$1++) { + vm.$off(event[i$1], fn); + } + return vm; + } + var cbs = vm._events[event]; + if (!cbs) { + return vm; + } + if (!fn) { + vm._events[event] = null; + return vm; + } + var cb; + var i = cbs.length; + while (i--) { + cb = cbs[i]; + if (cb === fn || cb.fn === fn) { + cbs.splice(i, 1); + break; + } + } + return vm; + }; + Vue2.prototype.$emit = function(event) { + var vm = this; + if (true) { + var lowerCaseEvent = event.toLowerCase(); + if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { + tip('Event "' + lowerCaseEvent + '" is emitted in component ' + formatComponentName(vm) + ' but the handler is registered for "' + event + '". Note that HTML attributes are case-insensitive and you cannot use v-on to listen to camelCase events when using in-DOM templates. You should probably use "' + hyphenate(event) + '" instead of "' + event + '".'); + } + } + var cbs = vm._events[event]; + if (cbs) { + cbs = cbs.length > 1 ? toArray(cbs) : cbs; + var args = toArray(arguments, 1); + var info = 'event handler for "' + event + '"'; + for (var i = 0, l = cbs.length; i < l; i++) { + invokeWithErrorHandling(cbs[i], vm, args, vm, info); + } + } + return vm; + }; + } + var activeInstance = null; + var isUpdatingChildComponent = false; + function setActiveInstance(vm) { + var prevActiveInstance = activeInstance; + activeInstance = vm; + return function() { + activeInstance = prevActiveInstance; + }; + } + function initLifecycle(vm) { + var options = vm.$options; + var parent = options.parent; + if (parent && !options.abstract) { + while (parent.$options.abstract && parent.$parent) { + parent = parent.$parent; + } + parent.$children.push(vm); + } + vm.$parent = parent; + vm.$root = parent ? parent.$root : vm; + vm.$children = []; + vm.$refs = {}; + vm._watcher = null; + vm._inactive = null; + vm._directInactive = false; + vm._isMounted = false; + vm._isDestroyed = false; + vm._isBeingDestroyed = false; + } + function lifecycleMixin(Vue2) { + Vue2.prototype._update = function(vnode, hydrating) { + var vm = this; + var prevEl = vm.$el; + var prevVnode = vm._vnode; + var restoreActiveInstance = setActiveInstance(vm); + vm._vnode = vnode; + if (!prevVnode) { + vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false); + } else { + vm.$el = vm.__patch__(prevVnode, vnode); + } + restoreActiveInstance(); + if (prevEl) { + prevEl.__vue__ = null; + } + if (vm.$el) { + vm.$el.__vue__ = vm; + } + if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { + vm.$parent.$el = vm.$el; + } + }; + Vue2.prototype.$forceUpdate = function() { + var vm = this; + if (vm._watcher) { + vm._watcher.update(); + } + }; + Vue2.prototype.$destroy = function() { + var vm = this; + if (vm._isBeingDestroyed) { + return; + } + callHook(vm, "beforeDestroy"); + vm._isBeingDestroyed = true; + var parent = vm.$parent; + if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { + remove(parent.$children, vm); + } + if (vm._watcher) { + vm._watcher.teardown(); + } + var i = vm._watchers.length; + while (i--) { + vm._watchers[i].teardown(); + } + if (vm._data.__ob__) { + vm._data.__ob__.vmCount--; + } + vm._isDestroyed = true; + vm.__patch__(vm._vnode, null); + callHook(vm, "destroyed"); + vm.$off(); + if (vm.$el) { + vm.$el.__vue__ = null; + } + if (vm.$vnode) { + vm.$vnode.parent = null; + } + }; + } + function mountComponent(vm, el, hydrating) { + vm.$el = el; + if (!vm.$options.render) { + vm.$options.render = createEmptyVNode; + if (true) { + if (vm.$options.template && vm.$options.template.charAt(0) !== "#" || vm.$options.el || el) { + warn("You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.", vm); + } else { + warn("Failed to mount component: template or render function not defined.", vm); + } + } + } + callHook(vm, "beforeMount"); + var updateComponent; + if (config.performance && mark) { + updateComponent = function() { + var name = vm._name; + var id = vm._uid; + var startTag = "vue-perf-start:" + id; + var endTag2 = "vue-perf-end:" + id; + mark(startTag); + var vnode = vm._render(); + mark(endTag2); + measure("vue " + name + " render", startTag, endTag2); + mark(startTag); + vm._update(vnode, hydrating); + mark(endTag2); + measure("vue " + name + " patch", startTag, endTag2); + }; + } else { + updateComponent = function() { + vm._update(vm._render(), hydrating); + }; + } + new Watcher(vm, updateComponent, noop, { + before: function before() { + if (vm._isMounted && !vm._isDestroyed) { + callHook(vm, "beforeUpdate"); + } + } + }, true); + hydrating = false; + if (vm.$vnode == null) { + vm._isMounted = true; + callHook(vm, "mounted"); + } + return vm; + } + function updateChildComponent(vm, propsData, listeners, parentVnode, renderChildren) { + if (true) { + isUpdatingChildComponent = true; + } + var newScopedSlots = parentVnode.data.scopedSlots; + var oldScopedSlots = vm.$scopedSlots; + var hasDynamicScopedSlot = !!(newScopedSlots && !newScopedSlots.$stable || oldScopedSlots !== emptyObject && !oldScopedSlots.$stable || newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key); + var needsForceUpdate = !!(renderChildren || vm.$options._renderChildren || hasDynamicScopedSlot); + vm.$options._parentVnode = parentVnode; + vm.$vnode = parentVnode; + if (vm._vnode) { + vm._vnode.parent = parentVnode; + } + vm.$options._renderChildren = renderChildren; + vm.$attrs = parentVnode.data.attrs || emptyObject; + vm.$listeners = listeners || emptyObject; + if (propsData && vm.$options.props) { + toggleObserving(false); + var props2 = vm._props; + var propKeys = vm.$options._propKeys || []; + for (var i = 0; i < propKeys.length; i++) { + var key = propKeys[i]; + var propOptions = vm.$options.props; + props2[key] = validateProp(key, propOptions, propsData, vm); + } + toggleObserving(true); + vm.$options.propsData = propsData; + } + listeners = listeners || emptyObject; + var oldListeners = vm.$options._parentListeners; + vm.$options._parentListeners = listeners; + updateComponentListeners(vm, listeners, oldListeners); + if (needsForceUpdate) { + vm.$slots = resolveSlots(renderChildren, parentVnode.context); + vm.$forceUpdate(); + } + if (true) { + isUpdatingChildComponent = false; + } + } + function isInInactiveTree(vm) { + while (vm && (vm = vm.$parent)) { + if (vm._inactive) { + return true; + } + } + return false; + } + function activateChildComponent(vm, direct) { + if (direct) { + vm._directInactive = false; + if (isInInactiveTree(vm)) { + return; + } + } else if (vm._directInactive) { + return; + } + if (vm._inactive || vm._inactive === null) { + vm._inactive = false; + for (var i = 0; i < vm.$children.length; i++) { + activateChildComponent(vm.$children[i]); + } + callHook(vm, "activated"); + } + } + function deactivateChildComponent(vm, direct) { + if (direct) { + vm._directInactive = true; + if (isInInactiveTree(vm)) { + return; + } + } + if (!vm._inactive) { + vm._inactive = true; + for (var i = 0; i < vm.$children.length; i++) { + deactivateChildComponent(vm.$children[i]); + } + callHook(vm, "deactivated"); + } + } + function callHook(vm, hook) { + pushTarget(); + var handlers = vm.$options[hook]; + var info = hook + " hook"; + if (handlers) { + for (var i = 0, j = handlers.length; i < j; i++) { + invokeWithErrorHandling(handlers[i], vm, null, vm, info); + } + } + if (vm._hasHookEvent) { + vm.$emit("hook:" + hook); + } + popTarget(); + } + var MAX_UPDATE_COUNT = 100; + var queue = []; + var activatedChildren = []; + var has = {}; + var circular = {}; + var waiting = false; + var flushing = false; + var index = 0; + function resetSchedulerState() { + index = queue.length = activatedChildren.length = 0; + has = {}; + if (true) { + circular = {}; + } + waiting = flushing = false; + } + var currentFlushTimestamp = 0; + var getNow = Date.now; + if (inBrowser && !isIE) { + performance = window.performance; + if (performance && typeof performance.now === "function" && getNow() > document.createEvent("Event").timeStamp) { + getNow = function() { + return performance.now(); + }; + } + } + var performance; + function flushSchedulerQueue() { + currentFlushTimestamp = getNow(); + flushing = true; + var watcher, id; + queue.sort(function(a, b) { + return a.id - b.id; + }); + for (index = 0; index < queue.length; index++) { + watcher = queue[index]; + if (watcher.before) { + watcher.before(); + } + id = watcher.id; + has[id] = null; + watcher.run(); + if (has[id] != null) { + circular[id] = (circular[id] || 0) + 1; + if (circular[id] > MAX_UPDATE_COUNT) { + warn("You may have an infinite update loop " + (watcher.user ? 'in watcher with expression "' + watcher.expression + '"' : "in a component render function."), watcher.vm); + break; + } + } + } + var activatedQueue = activatedChildren.slice(); + var updatedQueue = queue.slice(); + resetSchedulerState(); + callActivatedHooks(activatedQueue); + callUpdatedHooks(updatedQueue); + if (devtools && config.devtools) { + devtools.emit("flush"); + } + } + function callUpdatedHooks(queue2) { + var i = queue2.length; + while (i--) { + var watcher = queue2[i]; + var vm = watcher.vm; + if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) { + callHook(vm, "updated"); + } + } + } + function queueActivatedComponent(vm) { + vm._inactive = false; + activatedChildren.push(vm); + } + function callActivatedHooks(queue2) { + for (var i = 0; i < queue2.length; i++) { + queue2[i]._inactive = true; + activateChildComponent(queue2[i], true); + } + } + function queueWatcher(watcher) { + var id = watcher.id; + if (has[id] == null) { + has[id] = true; + if (!flushing) { + queue.push(watcher); + } else { + var i = queue.length - 1; + while (i > index && queue[i].id > watcher.id) { + i--; + } + queue.splice(i + 1, 0, watcher); + } + if (!waiting) { + waiting = true; + if (!config.async) { + flushSchedulerQueue(); + return; + } + nextTick(flushSchedulerQueue); + } + } + } + var uid$2 = 0; + var Watcher = function Watcher2(vm, expOrFn, cb, options, isRenderWatcher) { + this.vm = vm; + if (isRenderWatcher) { + vm._watcher = this; + } + vm._watchers.push(this); + if (options) { + this.deep = !!options.deep; + this.user = !!options.user; + this.lazy = !!options.lazy; + this.sync = !!options.sync; + this.before = options.before; + } else { + this.deep = this.user = this.lazy = this.sync = false; + } + this.cb = cb; + this.id = ++uid$2; + this.active = true; + this.dirty = this.lazy; + this.deps = []; + this.newDeps = []; + this.depIds = new _Set(); + this.newDepIds = new _Set(); + this.expression = true ? expOrFn.toString() : ""; + if (typeof expOrFn === "function") { + this.getter = expOrFn; + } else { + this.getter = parsePath(expOrFn); + if (!this.getter) { + this.getter = noop; + warn('Failed watching path: "' + expOrFn + '" Watcher only accepts simple dot-delimited paths. For full control, use a function instead.', vm); + } + } + this.value = this.lazy ? void 0 : this.get(); + }; + Watcher.prototype.get = function get() { + pushTarget(this); + var value; + var vm = this.vm; + try { + value = this.getter.call(vm, vm); + } catch (e) { + if (this.user) { + handleError(e, vm, 'getter for watcher "' + this.expression + '"'); + } else { + throw e; + } + } finally { + if (this.deep) { + traverse(value); + } + popTarget(); + this.cleanupDeps(); + } + return value; + }; + Watcher.prototype.addDep = function addDep(dep) { + var id = dep.id; + if (!this.newDepIds.has(id)) { + this.newDepIds.add(id); + this.newDeps.push(dep); + if (!this.depIds.has(id)) { + dep.addSub(this); + } + } + }; + Watcher.prototype.cleanupDeps = function cleanupDeps() { + var i = this.deps.length; + while (i--) { + var dep = this.deps[i]; + if (!this.newDepIds.has(dep.id)) { + dep.removeSub(this); + } + } + var tmp = this.depIds; + this.depIds = this.newDepIds; + this.newDepIds = tmp; + this.newDepIds.clear(); + tmp = this.deps; + this.deps = this.newDeps; + this.newDeps = tmp; + this.newDeps.length = 0; + }; + Watcher.prototype.update = function update() { + if (this.lazy) { + this.dirty = true; + } else if (this.sync) { + this.run(); + } else { + queueWatcher(this); + } + }; + Watcher.prototype.run = function run() { + if (this.active) { + var value = this.get(); + if (value !== this.value || isObject(value) || this.deep) { + var oldValue = this.value; + this.value = value; + if (this.user) { + try { + this.cb.call(this.vm, value, oldValue); + } catch (e) { + handleError(e, this.vm, 'callback for watcher "' + this.expression + '"'); + } + } else { + this.cb.call(this.vm, value, oldValue); + } + } + } + }; + Watcher.prototype.evaluate = function evaluate() { + this.value = this.get(); + this.dirty = false; + }; + Watcher.prototype.depend = function depend2() { + var i = this.deps.length; + while (i--) { + this.deps[i].depend(); + } + }; + Watcher.prototype.teardown = function teardown() { + if (this.active) { + if (!this.vm._isBeingDestroyed) { + remove(this.vm._watchers, this); + } + var i = this.deps.length; + while (i--) { + this.deps[i].removeSub(this); + } + this.active = false; + } + }; + var sharedPropertyDefinition = { + enumerable: true, + configurable: true, + get: noop, + set: noop + }; + function proxy(target2, sourceKey, key) { + sharedPropertyDefinition.get = function proxyGetter() { + return this[sourceKey][key]; + }; + sharedPropertyDefinition.set = function proxySetter(val) { + this[sourceKey][key] = val; + }; + Object.defineProperty(target2, key, sharedPropertyDefinition); + } + function initState(vm) { + vm._watchers = []; + var opts = vm.$options; + if (opts.props) { + initProps(vm, opts.props); + } + if (opts.methods) { + initMethods(vm, opts.methods); + } + if (opts.data) { + initData(vm); + } else { + observe(vm._data = {}, true); + } + if (opts.computed) { + initComputed(vm, opts.computed); + } + if (opts.watch && opts.watch !== nativeWatch) { + initWatch(vm, opts.watch); + } + } + function initProps(vm, propsOptions) { + var propsData = vm.$options.propsData || {}; + var props2 = vm._props = {}; + var keys = vm.$options._propKeys = []; + var isRoot = !vm.$parent; + if (!isRoot) { + toggleObserving(false); + } + var loop = function(key2) { + keys.push(key2); + var value = validateProp(key2, propsOptions, propsData, vm); + if (true) { + var hyphenatedKey = hyphenate(key2); + if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) { + warn('"' + hyphenatedKey + '" is a reserved attribute and cannot be used as component prop.', vm); + } + defineReactive$$1(props2, key2, value, function() { + if (!isRoot && !isUpdatingChildComponent) { + warn(`Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "` + key2 + '"', vm); + } + }); + } else { + defineReactive$$1(props2, key2, value); + } + if (!(key2 in vm)) { + proxy(vm, "_props", key2); + } + }; + for (var key in propsOptions) + loop(key); + toggleObserving(true); + } + function initData(vm) { + var data = vm.$options.data; + data = vm._data = typeof data === "function" ? getData(data, vm) : data || {}; + if (!isPlainObject(data)) { + data = {}; + warn("data functions should return an object:\nhttps://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function", vm); + } + var keys = Object.keys(data); + var props2 = vm.$options.props; + var methods = vm.$options.methods; + var i = keys.length; + while (i--) { + var key = keys[i]; + if (true) { + if (methods && hasOwn(methods, key)) { + warn('Method "' + key + '" has already been defined as a data property.', vm); + } + } + if (props2 && hasOwn(props2, key)) { + warn('The data property "' + key + '" is already declared as a prop. Use prop default value instead.', vm); + } else if (!isReserved(key)) { + proxy(vm, "_data", key); + } + } + observe(data, true); + } + function getData(data, vm) { + pushTarget(); + try { + return data.call(vm, vm); + } catch (e) { + handleError(e, vm, "data()"); + return {}; + } finally { + popTarget(); + } + } + var computedWatcherOptions = { lazy: true }; + function initComputed(vm, computed) { + var watchers = vm._computedWatchers = /* @__PURE__ */ Object.create(null); + var isSSR = isServerRendering(); + for (var key in computed) { + var userDef = computed[key]; + var getter = typeof userDef === "function" ? userDef : userDef.get; + if (getter == null) { + warn('Getter is missing for computed property "' + key + '".', vm); + } + if (!isSSR) { + watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions); + } + if (!(key in vm)) { + defineComputed(vm, key, userDef); + } else if (true) { + if (key in vm.$data) { + warn('The computed property "' + key + '" is already defined in data.', vm); + } else if (vm.$options.props && key in vm.$options.props) { + warn('The computed property "' + key + '" is already defined as a prop.', vm); + } + } + } + } + function defineComputed(target2, key, userDef) { + var shouldCache = !isServerRendering(); + if (typeof userDef === "function") { + sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef); + sharedPropertyDefinition.set = noop; + } else { + sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop; + sharedPropertyDefinition.set = userDef.set || noop; + } + if (sharedPropertyDefinition.set === noop) { + sharedPropertyDefinition.set = function() { + warn('Computed property "' + key + '" was assigned to but it has no setter.', this); + }; + } + Object.defineProperty(target2, key, sharedPropertyDefinition); + } + function createComputedGetter(key) { + return function computedGetter() { + var watcher = this._computedWatchers && this._computedWatchers[key]; + if (watcher) { + if (watcher.dirty) { + watcher.evaluate(); + } + if (Dep.target) { + watcher.depend(); + } + return watcher.value; + } + }; + } + function createGetterInvoker(fn) { + return function computedGetter() { + return fn.call(this, this); + }; + } + function initMethods(vm, methods) { + var props2 = vm.$options.props; + for (var key in methods) { + if (true) { + if (typeof methods[key] !== "function") { + warn('Method "' + key + '" has type "' + typeof methods[key] + '" in the component definition. Did you reference the function correctly?', vm); + } + if (props2 && hasOwn(props2, key)) { + warn('Method "' + key + '" has already been defined as a prop.', vm); + } + if (key in vm && isReserved(key)) { + warn('Method "' + key + '" conflicts with an existing Vue instance method. Avoid defining component methods that start with _ or $.'); + } + } + vm[key] = typeof methods[key] !== "function" ? noop : bind(methods[key], vm); + } + } + function initWatch(vm, watch) { + for (var key in watch) { + var handler = watch[key]; + if (Array.isArray(handler)) { + for (var i = 0; i < handler.length; i++) { + createWatcher(vm, key, handler[i]); + } + } else { + createWatcher(vm, key, handler); + } + } + } + function createWatcher(vm, expOrFn, handler, options) { + if (isPlainObject(handler)) { + options = handler; + handler = handler.handler; + } + if (typeof handler === "string") { + handler = vm[handler]; + } + return vm.$watch(expOrFn, handler, options); + } + function stateMixin(Vue2) { + var dataDef = {}; + dataDef.get = function() { + return this._data; + }; + var propsDef = {}; + propsDef.get = function() { + return this._props; + }; + if (true) { + dataDef.set = function() { + warn("Avoid replacing instance root $data. Use nested data properties instead.", this); + }; + propsDef.set = function() { + warn("$props is readonly.", this); + }; + } + Object.defineProperty(Vue2.prototype, "$data", dataDef); + Object.defineProperty(Vue2.prototype, "$props", propsDef); + Vue2.prototype.$set = set; + Vue2.prototype.$delete = del; + Vue2.prototype.$watch = function(expOrFn, cb, options) { + var vm = this; + if (isPlainObject(cb)) { + return createWatcher(vm, expOrFn, cb, options); + } + options = options || {}; + options.user = true; + var watcher = new Watcher(vm, expOrFn, cb, options); + if (options.immediate) { + try { + cb.call(vm, watcher.value); + } catch (error) { + handleError(error, vm, 'callback for immediate watcher "' + watcher.expression + '"'); + } + } + return function unwatchFn() { + watcher.teardown(); + }; + }; + } + var uid$3 = 0; + function initMixin(Vue2) { + Vue2.prototype._init = function(options) { + var vm = this; + vm._uid = uid$3++; + var startTag, endTag2; + if (config.performance && mark) { + startTag = "vue-perf-start:" + vm._uid; + endTag2 = "vue-perf-end:" + vm._uid; + mark(startTag); + } + vm._isVue = true; + if (options && options._isComponent) { + initInternalComponent(vm, options); + } else { + vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm); + } + if (true) { + initProxy(vm); + } else { + vm._renderProxy = vm; + } + vm._self = vm; + initLifecycle(vm); + initEvents(vm); + initRender(vm); + callHook(vm, "beforeCreate"); + initInjections(vm); + initState(vm); + initProvide(vm); + callHook(vm, "created"); + if (config.performance && mark) { + vm._name = formatComponentName(vm, false); + mark(endTag2); + measure("vue " + vm._name + " init", startTag, endTag2); + } + if (vm.$options.el) { + vm.$mount(vm.$options.el); + } + }; + } + function initInternalComponent(vm, options) { + var opts = vm.$options = Object.create(vm.constructor.options); + var parentVnode = options._parentVnode; + opts.parent = options.parent; + opts._parentVnode = parentVnode; + var vnodeComponentOptions = parentVnode.componentOptions; + opts.propsData = vnodeComponentOptions.propsData; + opts._parentListeners = vnodeComponentOptions.listeners; + opts._renderChildren = vnodeComponentOptions.children; + opts._componentTag = vnodeComponentOptions.tag; + if (options.render) { + opts.render = options.render; + opts.staticRenderFns = options.staticRenderFns; + } + } + function resolveConstructorOptions(Ctor) { + var options = Ctor.options; + if (Ctor.super) { + var superOptions = resolveConstructorOptions(Ctor.super); + var cachedSuperOptions = Ctor.superOptions; + if (superOptions !== cachedSuperOptions) { + Ctor.superOptions = superOptions; + var modifiedOptions = resolveModifiedOptions(Ctor); + if (modifiedOptions) { + extend(Ctor.extendOptions, modifiedOptions); + } + options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions); + if (options.name) { + options.components[options.name] = Ctor; + } + } + } + return options; + } + function resolveModifiedOptions(Ctor) { + var modified; + var latest = Ctor.options; + var sealed = Ctor.sealedOptions; + for (var key in latest) { + if (latest[key] !== sealed[key]) { + if (!modified) { + modified = {}; + } + modified[key] = latest[key]; + } + } + return modified; + } + function Vue(options) { + if (!(this instanceof Vue)) { + warn("Vue is a constructor and should be called with the `new` keyword"); + } + this._init(options); + } + initMixin(Vue); + stateMixin(Vue); + eventsMixin(Vue); + lifecycleMixin(Vue); + renderMixin(Vue); + function initUse(Vue2) { + Vue2.use = function(plugin) { + var installedPlugins = this._installedPlugins || (this._installedPlugins = []); + if (installedPlugins.indexOf(plugin) > -1) { + return this; + } + var args = toArray(arguments, 1); + args.unshift(this); + if (typeof plugin.install === "function") { + plugin.install.apply(plugin, args); + } else if (typeof plugin === "function") { + plugin.apply(null, args); + } + installedPlugins.push(plugin); + return this; + }; + } + function initMixin$1(Vue2) { + Vue2.mixin = function(mixin) { + this.options = mergeOptions(this.options, mixin); + return this; + }; + } + function initExtend(Vue2) { + Vue2.cid = 0; + var cid = 1; + Vue2.extend = function(extendOptions) { + extendOptions = extendOptions || {}; + var Super = this; + var SuperId = Super.cid; + var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}); + if (cachedCtors[SuperId]) { + return cachedCtors[SuperId]; + } + var name = extendOptions.name || Super.options.name; + if (name) { + validateComponentName(name); + } + var Sub = function VueComponent(options) { + this._init(options); + }; + Sub.prototype = Object.create(Super.prototype); + Sub.prototype.constructor = Sub; + Sub.cid = cid++; + Sub.options = mergeOptions(Super.options, extendOptions); + Sub["super"] = Super; + if (Sub.options.props) { + initProps$1(Sub); + } + if (Sub.options.computed) { + initComputed$1(Sub); + } + Sub.extend = Super.extend; + Sub.mixin = Super.mixin; + Sub.use = Super.use; + ASSET_TYPES.forEach(function(type) { + Sub[type] = Super[type]; + }); + if (name) { + Sub.options.components[name] = Sub; + } + Sub.superOptions = Super.options; + Sub.extendOptions = extendOptions; + Sub.sealedOptions = extend({}, Sub.options); + cachedCtors[SuperId] = Sub; + return Sub; + }; + } + function initProps$1(Comp) { + var props2 = Comp.options.props; + for (var key in props2) { + proxy(Comp.prototype, "_props", key); + } + } + function initComputed$1(Comp) { + var computed = Comp.options.computed; + for (var key in computed) { + defineComputed(Comp.prototype, key, computed[key]); + } + } + function initAssetRegisters(Vue2) { + ASSET_TYPES.forEach(function(type) { + Vue2[type] = function(id, definition) { + if (!definition) { + return this.options[type + "s"][id]; + } else { + if (type === "component") { + validateComponentName(id); + } + if (type === "component" && isPlainObject(definition)) { + definition.name = definition.name || id; + definition = this.options._base.extend(definition); + } + if (type === "directive" && typeof definition === "function") { + definition = { bind: definition, update: definition }; + } + this.options[type + "s"][id] = definition; + return definition; + } + }; + }); + } + function getComponentName(opts) { + return opts && (opts.Ctor.options.name || opts.tag); + } + function matches(pattern, name) { + if (Array.isArray(pattern)) { + return pattern.indexOf(name) > -1; + } else if (typeof pattern === "string") { + return pattern.split(",").indexOf(name) > -1; + } else if (isRegExp(pattern)) { + return pattern.test(name); + } + return false; + } + function pruneCache(keepAliveInstance, filter) { + var cache = keepAliveInstance.cache; + var keys = keepAliveInstance.keys; + var _vnode = keepAliveInstance._vnode; + for (var key in cache) { + var cachedNode = cache[key]; + if (cachedNode) { + var name = getComponentName(cachedNode.componentOptions); + if (name && !filter(name)) { + pruneCacheEntry(cache, key, keys, _vnode); + } + } + } + } + function pruneCacheEntry(cache, key, keys, current) { + var cached$$1 = cache[key]; + if (cached$$1 && (!current || cached$$1.tag !== current.tag)) { + cached$$1.componentInstance.$destroy(); + } + cache[key] = null; + remove(keys, key); + } + var patternTypes = [String, RegExp, Array]; + var KeepAlive = { + name: "keep-alive", + abstract: true, + props: { + include: patternTypes, + exclude: patternTypes, + max: [String, Number] + }, + created: function created() { + this.cache = /* @__PURE__ */ Object.create(null); + this.keys = []; + }, + destroyed: function destroyed() { + for (var key in this.cache) { + pruneCacheEntry(this.cache, key, this.keys); + } + }, + mounted: function mounted() { + var this$1 = this; + this.$watch("include", function(val) { + pruneCache(this$1, function(name) { + return matches(val, name); + }); + }); + this.$watch("exclude", function(val) { + pruneCache(this$1, function(name) { + return !matches(val, name); + }); + }); + }, + render: function render() { + var slot = this.$slots.default; + var vnode = getFirstComponentChild(slot); + var componentOptions = vnode && vnode.componentOptions; + if (componentOptions) { + var name = getComponentName(componentOptions); + var ref2 = this; + var include = ref2.include; + var exclude = ref2.exclude; + if (include && (!name || !matches(include, name)) || exclude && name && matches(exclude, name)) { + return vnode; + } + var ref$12 = this; + var cache = ref$12.cache; + var keys = ref$12.keys; + var key = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? "::" + componentOptions.tag : "") : vnode.key; + if (cache[key]) { + vnode.componentInstance = cache[key].componentInstance; + remove(keys, key); + keys.push(key); + } else { + cache[key] = vnode; + keys.push(key); + if (this.max && keys.length > parseInt(this.max)) { + pruneCacheEntry(cache, keys[0], keys, this._vnode); + } + } + vnode.data.keepAlive = true; + } + return vnode || slot && slot[0]; + } + }; + var builtInComponents = { + KeepAlive + }; + function initGlobalAPI(Vue2) { + var configDef = {}; + configDef.get = function() { + return config; + }; + if (true) { + configDef.set = function() { + warn("Do not replace the Vue.config object, set individual fields instead."); + }; + } + Object.defineProperty(Vue2, "config", configDef); + Vue2.util = { + warn, + extend, + mergeOptions, + defineReactive: defineReactive$$1 + }; + Vue2.set = set; + Vue2.delete = del; + Vue2.nextTick = nextTick; + Vue2.observable = function(obj) { + observe(obj); + return obj; + }; + Vue2.options = /* @__PURE__ */ Object.create(null); + ASSET_TYPES.forEach(function(type) { + Vue2.options[type + "s"] = /* @__PURE__ */ Object.create(null); + }); + Vue2.options._base = Vue2; + extend(Vue2.options.components, builtInComponents); + initUse(Vue2); + initMixin$1(Vue2); + initExtend(Vue2); + initAssetRegisters(Vue2); + } + initGlobalAPI(Vue); + Object.defineProperty(Vue.prototype, "$isServer", { + get: isServerRendering + }); + Object.defineProperty(Vue.prototype, "$ssrContext", { + get: function get2() { + return this.$vnode && this.$vnode.ssrContext; + } + }); + Object.defineProperty(Vue, "FunctionalRenderContext", { + value: FunctionalRenderContext + }); + Vue.version = "2.6.12"; + var isReservedAttr = makeMap("style,class"); + var acceptValue = makeMap("input,textarea,option,select,progress"); + var mustUseProp = function(tag, type, attr) { + return attr === "value" && acceptValue(tag) && type !== "button" || attr === "selected" && tag === "option" || attr === "checked" && tag === "input" || attr === "muted" && tag === "video"; + }; + var isEnumeratedAttr = makeMap("contenteditable,draggable,spellcheck"); + var isValidContentEditableValue = makeMap("events,caret,typing,plaintext-only"); + var convertEnumeratedValue = function(key, value) { + return isFalsyAttrValue(value) || value === "false" ? "false" : key === "contenteditable" && isValidContentEditableValue(value) ? value : "true"; + }; + var isBooleanAttr = makeMap("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"); + var xlinkNS = "http://www.w3.org/1999/xlink"; + var isXlink = function(name) { + return name.charAt(5) === ":" && name.slice(0, 5) === "xlink"; + }; + var getXlinkProp = function(name) { + return isXlink(name) ? name.slice(6, name.length) : ""; + }; + var isFalsyAttrValue = function(val) { + return val == null || val === false; + }; + function genClassForVnode(vnode) { + var data = vnode.data; + var parentNode2 = vnode; + var childNode = vnode; + while (isDef(childNode.componentInstance)) { + childNode = childNode.componentInstance._vnode; + if (childNode && childNode.data) { + data = mergeClassData(childNode.data, data); + } + } + while (isDef(parentNode2 = parentNode2.parent)) { + if (parentNode2 && parentNode2.data) { + data = mergeClassData(data, parentNode2.data); + } + } + return renderClass(data.staticClass, data.class); + } + function mergeClassData(child, parent) { + return { + staticClass: concat(child.staticClass, parent.staticClass), + class: isDef(child.class) ? [child.class, parent.class] : parent.class + }; + } + function renderClass(staticClass, dynamicClass) { + if (isDef(staticClass) || isDef(dynamicClass)) { + return concat(staticClass, stringifyClass(dynamicClass)); + } + return ""; + } + function concat(a, b) { + return a ? b ? a + " " + b : a : b || ""; + } + function stringifyClass(value) { + if (Array.isArray(value)) { + return stringifyArray(value); + } + if (isObject(value)) { + return stringifyObject(value); + } + if (typeof value === "string") { + return value; + } + return ""; + } + function stringifyArray(value) { + var res = ""; + var stringified; + for (var i = 0, l = value.length; i < l; i++) { + if (isDef(stringified = stringifyClass(value[i])) && stringified !== "") { + if (res) { + res += " "; + } + res += stringified; + } + } + return res; + } + function stringifyObject(value) { + var res = ""; + for (var key in value) { + if (value[key]) { + if (res) { + res += " "; + } + res += key; + } + } + return res; + } + var namespaceMap = { + svg: "http://www.w3.org/2000/svg", + math: "http://www.w3.org/1998/Math/MathML" + }; + var isHTMLTag = makeMap("html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,menuitem,summary,content,element,shadow,template,blockquote,iframe,tfoot"); + var isSVG = makeMap("svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view", true); + var isPreTag = function(tag) { + return tag === "pre"; + }; + var isReservedTag = function(tag) { + return isHTMLTag(tag) || isSVG(tag); + }; + function getTagNamespace(tag) { + if (isSVG(tag)) { + return "svg"; + } + if (tag === "math") { + return "math"; + } + } + var unknownElementCache = /* @__PURE__ */ Object.create(null); + function isUnknownElement(tag) { + if (!inBrowser) { + return true; + } + if (isReservedTag(tag)) { + return false; + } + tag = tag.toLowerCase(); + if (unknownElementCache[tag] != null) { + return unknownElementCache[tag]; + } + var el = document.createElement(tag); + if (tag.indexOf("-") > -1) { + return unknownElementCache[tag] = el.constructor === window.HTMLUnknownElement || el.constructor === window.HTMLElement; + } else { + return unknownElementCache[tag] = /HTMLUnknownElement/.test(el.toString()); + } + } + var isTextInputType = makeMap("text,number,password,search,email,tel,url"); + function query(el) { + if (typeof el === "string") { + var selected = document.querySelector(el); + if (!selected) { + warn("Cannot find element: " + el); + return document.createElement("div"); + } + return selected; + } else { + return el; + } + } + function createElement$1(tagName2, vnode) { + var elm = document.createElement(tagName2); + if (tagName2 !== "select") { + return elm; + } + if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== void 0) { + elm.setAttribute("multiple", "multiple"); + } + return elm; + } + function createElementNS(namespace, tagName2) { + return document.createElementNS(namespaceMap[namespace], tagName2); + } + function createTextNode(text2) { + return document.createTextNode(text2); + } + function createComment(text2) { + return document.createComment(text2); + } + function insertBefore(parentNode2, newNode, referenceNode) { + parentNode2.insertBefore(newNode, referenceNode); + } + function removeChild(node, child) { + node.removeChild(child); + } + function appendChild(node, child) { + node.appendChild(child); + } + function parentNode(node) { + return node.parentNode; + } + function nextSibling(node) { + return node.nextSibling; + } + function tagName(node) { + return node.tagName; + } + function setTextContent(node, text2) { + node.textContent = text2; + } + function setStyleScope(node, scopeId) { + node.setAttribute(scopeId, ""); + } + var nodeOps = /* @__PURE__ */ Object.freeze({ + createElement: createElement$1, + createElementNS, + createTextNode, + createComment, + insertBefore, + removeChild, + appendChild, + parentNode, + nextSibling, + tagName, + setTextContent, + setStyleScope + }); + var ref = { + create: function create(_, vnode) { + registerRef(vnode); + }, + update: function update2(oldVnode, vnode) { + if (oldVnode.data.ref !== vnode.data.ref) { + registerRef(oldVnode, true); + registerRef(vnode); + } + }, + destroy: function destroy2(vnode) { + registerRef(vnode, true); + } + }; + function registerRef(vnode, isRemoval) { + var key = vnode.data.ref; + if (!isDef(key)) { + return; + } + var vm = vnode.context; + var ref2 = vnode.componentInstance || vnode.elm; + var refs = vm.$refs; + if (isRemoval) { + if (Array.isArray(refs[key])) { + remove(refs[key], ref2); + } else if (refs[key] === ref2) { + refs[key] = void 0; + } + } else { + if (vnode.data.refInFor) { + if (!Array.isArray(refs[key])) { + refs[key] = [ref2]; + } else if (refs[key].indexOf(ref2) < 0) { + refs[key].push(ref2); + } + } else { + refs[key] = ref2; + } + } + } + var emptyNode = new VNode("", {}, []); + var hooks = ["create", "activate", "update", "remove", "destroy"]; + function sameVnode(a, b) { + return a.key === b.key && (a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) || isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error)); + } + function sameInputType(a, b) { + if (a.tag !== "input") { + return true; + } + var i; + var typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type; + var typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type; + return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB); + } + function createKeyToOldIdx(children, beginIdx, endIdx) { + var i, key; + var map = {}; + for (i = beginIdx; i <= endIdx; ++i) { + key = children[i].key; + if (isDef(key)) { + map[key] = i; + } + } + return map; + } + function createPatchFunction(backend) { + var i, j; + var cbs = {}; + var modules2 = backend.modules; + var nodeOps2 = backend.nodeOps; + for (i = 0; i < hooks.length; ++i) { + cbs[hooks[i]] = []; + for (j = 0; j < modules2.length; ++j) { + if (isDef(modules2[j][hooks[i]])) { + cbs[hooks[i]].push(modules2[j][hooks[i]]); + } + } + } + function emptyNodeAt(elm) { + return new VNode(nodeOps2.tagName(elm).toLowerCase(), {}, [], void 0, elm); + } + function createRmCb(childElm, listeners) { + function remove$$12() { + if (--remove$$12.listeners === 0) { + removeNode(childElm); + } + } + remove$$12.listeners = listeners; + return remove$$12; + } + function removeNode(el) { + var parent = nodeOps2.parentNode(el); + if (isDef(parent)) { + nodeOps2.removeChild(parent, el); + } + } + function isUnknownElement$$1(vnode, inVPre) { + return !inVPre && !vnode.ns && !(config.ignoredElements.length && config.ignoredElements.some(function(ignore) { + return isRegExp(ignore) ? ignore.test(vnode.tag) : ignore === vnode.tag; + })) && config.isUnknownElement(vnode.tag); + } + var creatingElmInVPre = 0; + function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index2) { + if (isDef(vnode.elm) && isDef(ownerArray)) { + vnode = ownerArray[index2] = cloneVNode(vnode); + } + vnode.isRootInsert = !nested; + if (createComponent2(vnode, insertedVnodeQueue, parentElm, refElm)) { + return; + } + var data = vnode.data; + var children = vnode.children; + var tag = vnode.tag; + if (isDef(tag)) { + if (true) { + if (data && data.pre) { + creatingElmInVPre++; + } + if (isUnknownElement$$1(vnode, creatingElmInVPre)) { + warn("Unknown custom element: <" + tag + '> - did you register the component correctly? For recursive components, make sure to provide the "name" option.', vnode.context); + } + } + vnode.elm = vnode.ns ? nodeOps2.createElementNS(vnode.ns, tag) : nodeOps2.createElement(tag, vnode); + setScope(vnode); + { + createChildren(vnode, children, insertedVnodeQueue); + if (isDef(data)) { + invokeCreateHooks(vnode, insertedVnodeQueue); + } + insert2(parentElm, vnode.elm, refElm); + } + if (data && data.pre) { + creatingElmInVPre--; + } + } else if (isTrue(vnode.isComment)) { + vnode.elm = nodeOps2.createComment(vnode.text); + insert2(parentElm, vnode.elm, refElm); + } else { + vnode.elm = nodeOps2.createTextNode(vnode.text); + insert2(parentElm, vnode.elm, refElm); + } + } + function createComponent2(vnode, insertedVnodeQueue, parentElm, refElm) { + var i2 = vnode.data; + if (isDef(i2)) { + var isReactivated = isDef(vnode.componentInstance) && i2.keepAlive; + if (isDef(i2 = i2.hook) && isDef(i2 = i2.init)) { + i2(vnode, false); + } + if (isDef(vnode.componentInstance)) { + initComponent(vnode, insertedVnodeQueue); + insert2(parentElm, vnode.elm, refElm); + if (isTrue(isReactivated)) { + reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); + } + return true; + } + } + } + function initComponent(vnode, insertedVnodeQueue) { + if (isDef(vnode.data.pendingInsert)) { + insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert); + vnode.data.pendingInsert = null; + } + vnode.elm = vnode.componentInstance.$el; + if (isPatchable(vnode)) { + invokeCreateHooks(vnode, insertedVnodeQueue); + setScope(vnode); + } else { + registerRef(vnode); + insertedVnodeQueue.push(vnode); + } + } + function reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) { + var i2; + var innerNode = vnode; + while (innerNode.componentInstance) { + innerNode = innerNode.componentInstance._vnode; + if (isDef(i2 = innerNode.data) && isDef(i2 = i2.transition)) { + for (i2 = 0; i2 < cbs.activate.length; ++i2) { + cbs.activate[i2](emptyNode, innerNode); + } + insertedVnodeQueue.push(innerNode); + break; + } + } + insert2(parentElm, vnode.elm, refElm); + } + function insert2(parent, elm, ref$$1) { + if (isDef(parent)) { + if (isDef(ref$$1)) { + if (nodeOps2.parentNode(ref$$1) === parent) { + nodeOps2.insertBefore(parent, elm, ref$$1); + } + } else { + nodeOps2.appendChild(parent, elm); + } + } + } + function createChildren(vnode, children, insertedVnodeQueue) { + if (Array.isArray(children)) { + if (true) { + checkDuplicateKeys(children); + } + for (var i2 = 0; i2 < children.length; ++i2) { + createElm(children[i2], insertedVnodeQueue, vnode.elm, null, true, children, i2); + } + } else if (isPrimitive(vnode.text)) { + nodeOps2.appendChild(vnode.elm, nodeOps2.createTextNode(String(vnode.text))); + } + } + function isPatchable(vnode) { + while (vnode.componentInstance) { + vnode = vnode.componentInstance._vnode; + } + return isDef(vnode.tag); + } + function invokeCreateHooks(vnode, insertedVnodeQueue) { + for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) { + cbs.create[i$1](emptyNode, vnode); + } + i = vnode.data.hook; + if (isDef(i)) { + if (isDef(i.create)) { + i.create(emptyNode, vnode); + } + if (isDef(i.insert)) { + insertedVnodeQueue.push(vnode); + } + } + } + function setScope(vnode) { + var i2; + if (isDef(i2 = vnode.fnScopeId)) { + nodeOps2.setStyleScope(vnode.elm, i2); + } else { + var ancestor = vnode; + while (ancestor) { + if (isDef(i2 = ancestor.context) && isDef(i2 = i2.$options._scopeId)) { + nodeOps2.setStyleScope(vnode.elm, i2); + } + ancestor = ancestor.parent; + } + } + if (isDef(i2 = activeInstance) && i2 !== vnode.context && i2 !== vnode.fnContext && isDef(i2 = i2.$options._scopeId)) { + nodeOps2.setStyleScope(vnode.elm, i2); + } + } + function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) { + for (; startIdx <= endIdx; ++startIdx) { + createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx); + } + } + function invokeDestroyHook(vnode) { + var i2, j2; + var data = vnode.data; + if (isDef(data)) { + if (isDef(i2 = data.hook) && isDef(i2 = i2.destroy)) { + i2(vnode); + } + for (i2 = 0; i2 < cbs.destroy.length; ++i2) { + cbs.destroy[i2](vnode); + } + } + if (isDef(i2 = vnode.children)) { + for (j2 = 0; j2 < vnode.children.length; ++j2) { + invokeDestroyHook(vnode.children[j2]); + } + } + } + function removeVnodes(vnodes, startIdx, endIdx) { + for (; startIdx <= endIdx; ++startIdx) { + var ch = vnodes[startIdx]; + if (isDef(ch)) { + if (isDef(ch.tag)) { + removeAndInvokeRemoveHook(ch); + invokeDestroyHook(ch); + } else { + removeNode(ch.elm); + } + } + } + } + function removeAndInvokeRemoveHook(vnode, rm) { + if (isDef(rm) || isDef(vnode.data)) { + var i2; + var listeners = cbs.remove.length + 1; + if (isDef(rm)) { + rm.listeners += listeners; + } else { + rm = createRmCb(vnode.elm, listeners); + } + if (isDef(i2 = vnode.componentInstance) && isDef(i2 = i2._vnode) && isDef(i2.data)) { + removeAndInvokeRemoveHook(i2, rm); + } + for (i2 = 0; i2 < cbs.remove.length; ++i2) { + cbs.remove[i2](vnode, rm); + } + if (isDef(i2 = vnode.data.hook) && isDef(i2 = i2.remove)) { + i2(vnode, rm); + } else { + rm(); + } + } else { + removeNode(vnode.elm); + } + } + function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { + var oldStartIdx = 0; + var newStartIdx = 0; + var oldEndIdx = oldCh.length - 1; + var oldStartVnode = oldCh[0]; + var oldEndVnode = oldCh[oldEndIdx]; + var newEndIdx = newCh.length - 1; + var newStartVnode = newCh[0]; + var newEndVnode = newCh[newEndIdx]; + var oldKeyToIdx, idxInOld, vnodeToMove, refElm; + var canMove = !removeOnly; + if (true) { + checkDuplicateKeys(newCh); + } + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { + if (isUndef(oldStartVnode)) { + oldStartVnode = oldCh[++oldStartIdx]; + } else if (isUndef(oldEndVnode)) { + oldEndVnode = oldCh[--oldEndIdx]; + } else if (sameVnode(oldStartVnode, newStartVnode)) { + patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx); + oldStartVnode = oldCh[++oldStartIdx]; + newStartVnode = newCh[++newStartIdx]; + } else if (sameVnode(oldEndVnode, newEndVnode)) { + patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx); + oldEndVnode = oldCh[--oldEndIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldStartVnode, newEndVnode)) { + patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx); + canMove && nodeOps2.insertBefore(parentElm, oldStartVnode.elm, nodeOps2.nextSibling(oldEndVnode.elm)); + oldStartVnode = oldCh[++oldStartIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldEndVnode, newStartVnode)) { + patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx); + canMove && nodeOps2.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); + oldEndVnode = oldCh[--oldEndIdx]; + newStartVnode = newCh[++newStartIdx]; + } else { + if (isUndef(oldKeyToIdx)) { + oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); + } + idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx); + if (isUndef(idxInOld)) { + createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx); + } else { + vnodeToMove = oldCh[idxInOld]; + if (sameVnode(vnodeToMove, newStartVnode)) { + patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx); + oldCh[idxInOld] = void 0; + canMove && nodeOps2.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm); + } else { + createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx); + } + } + newStartVnode = newCh[++newStartIdx]; + } + } + if (oldStartIdx > oldEndIdx) { + refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm; + addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); + } else if (newStartIdx > newEndIdx) { + removeVnodes(oldCh, oldStartIdx, oldEndIdx); + } + } + function checkDuplicateKeys(children) { + var seenKeys = {}; + for (var i2 = 0; i2 < children.length; i2++) { + var vnode = children[i2]; + var key = vnode.key; + if (isDef(key)) { + if (seenKeys[key]) { + warn("Duplicate keys detected: '" + key + "'. This may cause an update error.", vnode.context); + } else { + seenKeys[key] = true; + } + } + } + } + function findIdxInOld(node, oldCh, start, end) { + for (var i2 = start; i2 < end; i2++) { + var c = oldCh[i2]; + if (isDef(c) && sameVnode(node, c)) { + return i2; + } + } + } + function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index2, removeOnly) { + if (oldVnode === vnode) { + return; + } + if (isDef(vnode.elm) && isDef(ownerArray)) { + vnode = ownerArray[index2] = cloneVNode(vnode); + } + var elm = vnode.elm = oldVnode.elm; + if (isTrue(oldVnode.isAsyncPlaceholder)) { + if (isDef(vnode.asyncFactory.resolved)) { + hydrate(oldVnode.elm, vnode, insertedVnodeQueue); + } else { + vnode.isAsyncPlaceholder = true; + } + return; + } + if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) { + vnode.componentInstance = oldVnode.componentInstance; + return; + } + var i2; + var data = vnode.data; + if (isDef(data) && isDef(i2 = data.hook) && isDef(i2 = i2.prepatch)) { + i2(oldVnode, vnode); + } + var oldCh = oldVnode.children; + var ch = vnode.children; + if (isDef(data) && isPatchable(vnode)) { + for (i2 = 0; i2 < cbs.update.length; ++i2) { + cbs.update[i2](oldVnode, vnode); + } + if (isDef(i2 = data.hook) && isDef(i2 = i2.update)) { + i2(oldVnode, vnode); + } + } + if (isUndef(vnode.text)) { + if (isDef(oldCh) && isDef(ch)) { + if (oldCh !== ch) { + updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); + } + } else if (isDef(ch)) { + if (true) { + checkDuplicateKeys(ch); + } + if (isDef(oldVnode.text)) { + nodeOps2.setTextContent(elm, ""); + } + addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); + } else if (isDef(oldCh)) { + removeVnodes(oldCh, 0, oldCh.length - 1); + } else if (isDef(oldVnode.text)) { + nodeOps2.setTextContent(elm, ""); + } + } else if (oldVnode.text !== vnode.text) { + nodeOps2.setTextContent(elm, vnode.text); + } + if (isDef(data)) { + if (isDef(i2 = data.hook) && isDef(i2 = i2.postpatch)) { + i2(oldVnode, vnode); + } + } + } + function invokeInsertHook(vnode, queue2, initial) { + if (isTrue(initial) && isDef(vnode.parent)) { + vnode.parent.data.pendingInsert = queue2; + } else { + for (var i2 = 0; i2 < queue2.length; ++i2) { + queue2[i2].data.hook.insert(queue2[i2]); + } + } + } + var hydrationBailed = false; + var isRenderedModule = makeMap("attrs,class,staticClass,staticStyle,key"); + function hydrate(elm, vnode, insertedVnodeQueue, inVPre) { + var i2; + var tag = vnode.tag; + var data = vnode.data; + var children = vnode.children; + inVPre = inVPre || data && data.pre; + vnode.elm = elm; + if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) { + vnode.isAsyncPlaceholder = true; + return true; + } + if (true) { + if (!assertNodeMatch(elm, vnode, inVPre)) { + return false; + } + } + if (isDef(data)) { + if (isDef(i2 = data.hook) && isDef(i2 = i2.init)) { + i2(vnode, true); + } + if (isDef(i2 = vnode.componentInstance)) { + initComponent(vnode, insertedVnodeQueue); + return true; + } + } + if (isDef(tag)) { + if (isDef(children)) { + if (!elm.hasChildNodes()) { + createChildren(vnode, children, insertedVnodeQueue); + } else { + if (isDef(i2 = data) && isDef(i2 = i2.domProps) && isDef(i2 = i2.innerHTML)) { + if (i2 !== elm.innerHTML) { + if (typeof console !== "undefined" && !hydrationBailed) { + hydrationBailed = true; + console.warn("Parent: ", elm); + console.warn("server innerHTML: ", i2); + console.warn("client innerHTML: ", elm.innerHTML); + } + return false; + } + } else { + var childrenMatch = true; + var childNode = elm.firstChild; + for (var i$1 = 0; i$1 < children.length; i$1++) { + if (!childNode || !hydrate(childNode, children[i$1], insertedVnodeQueue, inVPre)) { + childrenMatch = false; + break; + } + childNode = childNode.nextSibling; + } + if (!childrenMatch || childNode) { + if (typeof console !== "undefined" && !hydrationBailed) { + hydrationBailed = true; + console.warn("Parent: ", elm); + console.warn("Mismatching childNodes vs. VNodes: ", elm.childNodes, children); + } + return false; + } + } + } + } + if (isDef(data)) { + var fullInvoke = false; + for (var key in data) { + if (!isRenderedModule(key)) { + fullInvoke = true; + invokeCreateHooks(vnode, insertedVnodeQueue); + break; + } + } + if (!fullInvoke && data["class"]) { + traverse(data["class"]); + } + } + } else if (elm.data !== vnode.text) { + elm.data = vnode.text; + } + return true; + } + function assertNodeMatch(node, vnode, inVPre) { + if (isDef(vnode.tag)) { + return vnode.tag.indexOf("vue-component") === 0 || !isUnknownElement$$1(vnode, inVPre) && vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase()); + } else { + return node.nodeType === (vnode.isComment ? 8 : 3); + } + } + return function patch2(oldVnode, vnode, hydrating, removeOnly) { + if (isUndef(vnode)) { + if (isDef(oldVnode)) { + invokeDestroyHook(oldVnode); + } + return; + } + var isInitialPatch = false; + var insertedVnodeQueue = []; + if (isUndef(oldVnode)) { + isInitialPatch = true; + createElm(vnode, insertedVnodeQueue); + } else { + var isRealElement = isDef(oldVnode.nodeType); + if (!isRealElement && sameVnode(oldVnode, vnode)) { + patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly); + } else { + if (isRealElement) { + if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { + oldVnode.removeAttribute(SSR_ATTR); + hydrating = true; + } + if (isTrue(hydrating)) { + if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { + invokeInsertHook(vnode, insertedVnodeQueue, true); + return oldVnode; + } else if (true) { + warn("The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside

, or missing . Bailing hydration and performing full client-side render."); + } + } + oldVnode = emptyNodeAt(oldVnode); + } + var oldElm = oldVnode.elm; + var parentElm = nodeOps2.parentNode(oldElm); + createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps2.nextSibling(oldElm)); + if (isDef(vnode.parent)) { + var ancestor = vnode.parent; + var patchable = isPatchable(vnode); + while (ancestor) { + for (var i2 = 0; i2 < cbs.destroy.length; ++i2) { + cbs.destroy[i2](ancestor); + } + ancestor.elm = vnode.elm; + if (patchable) { + for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) { + cbs.create[i$1](emptyNode, ancestor); + } + var insert3 = ancestor.data.hook.insert; + if (insert3.merged) { + for (var i$2 = 1; i$2 < insert3.fns.length; i$2++) { + insert3.fns[i$2](); + } + } + } else { + registerRef(ancestor); + } + ancestor = ancestor.parent; + } + } + if (isDef(parentElm)) { + removeVnodes([oldVnode], 0, 0); + } else if (isDef(oldVnode.tag)) { + invokeDestroyHook(oldVnode); + } + } + } + invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); + return vnode.elm; + }; + } + var directives = { + create: updateDirectives, + update: updateDirectives, + destroy: function unbindDirectives(vnode) { + updateDirectives(vnode, emptyNode); + } + }; + function updateDirectives(oldVnode, vnode) { + if (oldVnode.data.directives || vnode.data.directives) { + _update(oldVnode, vnode); + } + } + function _update(oldVnode, vnode) { + var isCreate = oldVnode === emptyNode; + var isDestroy = vnode === emptyNode; + var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context); + var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context); + var dirsWithInsert = []; + var dirsWithPostpatch = []; + var key, oldDir, dir; + for (key in newDirs) { + oldDir = oldDirs[key]; + dir = newDirs[key]; + if (!oldDir) { + callHook$1(dir, "bind", vnode, oldVnode); + if (dir.def && dir.def.inserted) { + dirsWithInsert.push(dir); + } + } else { + dir.oldValue = oldDir.value; + dir.oldArg = oldDir.arg; + callHook$1(dir, "update", vnode, oldVnode); + if (dir.def && dir.def.componentUpdated) { + dirsWithPostpatch.push(dir); + } + } + } + if (dirsWithInsert.length) { + var callInsert = function() { + for (var i = 0; i < dirsWithInsert.length; i++) { + callHook$1(dirsWithInsert[i], "inserted", vnode, oldVnode); + } + }; + if (isCreate) { + mergeVNodeHook(vnode, "insert", callInsert); + } else { + callInsert(); + } + } + if (dirsWithPostpatch.length) { + mergeVNodeHook(vnode, "postpatch", function() { + for (var i = 0; i < dirsWithPostpatch.length; i++) { + callHook$1(dirsWithPostpatch[i], "componentUpdated", vnode, oldVnode); + } + }); + } + if (!isCreate) { + for (key in oldDirs) { + if (!newDirs[key]) { + callHook$1(oldDirs[key], "unbind", oldVnode, oldVnode, isDestroy); + } + } + } + } + var emptyModifiers = /* @__PURE__ */ Object.create(null); + function normalizeDirectives$1(dirs, vm) { + var res = /* @__PURE__ */ Object.create(null); + if (!dirs) { + return res; + } + var i, dir; + for (i = 0; i < dirs.length; i++) { + dir = dirs[i]; + if (!dir.modifiers) { + dir.modifiers = emptyModifiers; + } + res[getRawDirName(dir)] = dir; + dir.def = resolveAsset(vm.$options, "directives", dir.name, true); + } + return res; + } + function getRawDirName(dir) { + return dir.rawName || dir.name + "." + Object.keys(dir.modifiers || {}).join("."); + } + function callHook$1(dir, hook, vnode, oldVnode, isDestroy) { + var fn = dir.def && dir.def[hook]; + if (fn) { + try { + fn(vnode.elm, dir, vnode, oldVnode, isDestroy); + } catch (e) { + handleError(e, vnode.context, "directive " + dir.name + " " + hook + " hook"); + } + } + } + var baseModules = [ + ref, + directives + ]; + function updateAttrs(oldVnode, vnode) { + var opts = vnode.componentOptions; + if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) { + return; + } + if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { + return; + } + var key, cur, old; + var elm = vnode.elm; + var oldAttrs = oldVnode.data.attrs || {}; + var attrs2 = vnode.data.attrs || {}; + if (isDef(attrs2.__ob__)) { + attrs2 = vnode.data.attrs = extend({}, attrs2); + } + for (key in attrs2) { + cur = attrs2[key]; + old = oldAttrs[key]; + if (old !== cur) { + setAttr(elm, key, cur); + } + } + if ((isIE || isEdge) && attrs2.value !== oldAttrs.value) { + setAttr(elm, "value", attrs2.value); + } + for (key in oldAttrs) { + if (isUndef(attrs2[key])) { + if (isXlink(key)) { + elm.removeAttributeNS(xlinkNS, getXlinkProp(key)); + } else if (!isEnumeratedAttr(key)) { + elm.removeAttribute(key); + } + } + } + } + function setAttr(el, key, value) { + if (el.tagName.indexOf("-") > -1) { + baseSetAttr(el, key, value); + } else if (isBooleanAttr(key)) { + if (isFalsyAttrValue(value)) { + el.removeAttribute(key); + } else { + value = key === "allowfullscreen" && el.tagName === "EMBED" ? "true" : key; + el.setAttribute(key, value); + } + } else if (isEnumeratedAttr(key)) { + el.setAttribute(key, convertEnumeratedValue(key, value)); + } else if (isXlink(key)) { + if (isFalsyAttrValue(value)) { + el.removeAttributeNS(xlinkNS, getXlinkProp(key)); + } else { + el.setAttributeNS(xlinkNS, key, value); + } + } else { + baseSetAttr(el, key, value); + } + } + function baseSetAttr(el, key, value) { + if (isFalsyAttrValue(value)) { + el.removeAttribute(key); + } else { + if (isIE && !isIE9 && el.tagName === "TEXTAREA" && key === "placeholder" && value !== "" && !el.__ieph) { + var blocker = function(e) { + e.stopImmediatePropagation(); + el.removeEventListener("input", blocker); + }; + el.addEventListener("input", blocker); + el.__ieph = true; + } + el.setAttribute(key, value); + } + } + var attrs = { + create: updateAttrs, + update: updateAttrs + }; + function updateClass(oldVnode, vnode) { + var el = vnode.elm; + var data = vnode.data; + var oldData = oldVnode.data; + if (isUndef(data.staticClass) && isUndef(data.class) && (isUndef(oldData) || isUndef(oldData.staticClass) && isUndef(oldData.class))) { + return; + } + var cls = genClassForVnode(vnode); + var transitionClass = el._transitionClasses; + if (isDef(transitionClass)) { + cls = concat(cls, stringifyClass(transitionClass)); + } + if (cls !== el._prevClass) { + el.setAttribute("class", cls); + el._prevClass = cls; + } + } + var klass = { + create: updateClass, + update: updateClass + }; + var validDivisionCharRE = /[\w).+\-_$\]]/; + function parseFilters(exp) { + var inSingle = false; + var inDouble = false; + var inTemplateString = false; + var inRegex = false; + var curly = 0; + var square = 0; + var paren = 0; + var lastFilterIndex = 0; + var c, prev, i, expression, filters; + for (i = 0; i < exp.length; i++) { + prev = c; + c = exp.charCodeAt(i); + if (inSingle) { + if (c === 39 && prev !== 92) { + inSingle = false; + } + } else if (inDouble) { + if (c === 34 && prev !== 92) { + inDouble = false; + } + } else if (inTemplateString) { + if (c === 96 && prev !== 92) { + inTemplateString = false; + } + } else if (inRegex) { + if (c === 47 && prev !== 92) { + inRegex = false; + } + } else if (c === 124 && exp.charCodeAt(i + 1) !== 124 && exp.charCodeAt(i - 1) !== 124 && !curly && !square && !paren) { + if (expression === void 0) { + lastFilterIndex = i + 1; + expression = exp.slice(0, i).trim(); + } else { + pushFilter(); + } + } else { + switch (c) { + case 34: + inDouble = true; + break; + case 39: + inSingle = true; + break; + case 96: + inTemplateString = true; + break; + case 40: + paren++; + break; + case 41: + paren--; + break; + case 91: + square++; + break; + case 93: + square--; + break; + case 123: + curly++; + break; + case 125: + curly--; + break; + } + if (c === 47) { + var j = i - 1; + var p = void 0; + for (; j >= 0; j--) { + p = exp.charAt(j); + if (p !== " ") { + break; + } + } + if (!p || !validDivisionCharRE.test(p)) { + inRegex = true; + } + } + } + } + if (expression === void 0) { + expression = exp.slice(0, i).trim(); + } else if (lastFilterIndex !== 0) { + pushFilter(); + } + function pushFilter() { + (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim()); + lastFilterIndex = i + 1; + } + if (filters) { + for (i = 0; i < filters.length; i++) { + expression = wrapFilter(expression, filters[i]); + } + } + return expression; + } + function wrapFilter(exp, filter) { + var i = filter.indexOf("("); + if (i < 0) { + return '_f("' + filter + '")(' + exp + ")"; + } else { + var name = filter.slice(0, i); + var args = filter.slice(i + 1); + return '_f("' + name + '")(' + exp + (args !== ")" ? "," + args : args); + } + } + function baseWarn(msg, range2) { + console.error("[Vue compiler]: " + msg); + } + function pluckModuleFunction(modules2, key) { + return modules2 ? modules2.map(function(m) { + return m[key]; + }).filter(function(_) { + return _; + }) : []; + } + function addProp(el, name, value, range2, dynamic) { + (el.props || (el.props = [])).push(rangeSetItem({ name, value, dynamic }, range2)); + el.plain = false; + } + function addAttr(el, name, value, range2, dynamic) { + var attrs2 = dynamic ? el.dynamicAttrs || (el.dynamicAttrs = []) : el.attrs || (el.attrs = []); + attrs2.push(rangeSetItem({ name, value, dynamic }, range2)); + el.plain = false; + } + function addRawAttr(el, name, value, range2) { + el.attrsMap[name] = value; + el.attrsList.push(rangeSetItem({ name, value }, range2)); + } + function addDirective(el, name, rawName, value, arg, isDynamicArg, modifiers, range2) { + (el.directives || (el.directives = [])).push(rangeSetItem({ + name, + rawName, + value, + arg, + isDynamicArg, + modifiers + }, range2)); + el.plain = false; + } + function prependModifierMarker(symbol, name, dynamic) { + return dynamic ? "_p(" + name + ',"' + symbol + '")' : symbol + name; + } + function addHandler(el, name, value, modifiers, important, warn2, range2, dynamic) { + modifiers = modifiers || emptyObject; + if (warn2 && modifiers.prevent && modifiers.passive) { + warn2("passive and prevent can't be used together. Passive handler can't prevent default event.", range2); + } + if (modifiers.right) { + if (dynamic) { + name = "(" + name + ")==='click'?'contextmenu':(" + name + ")"; + } else if (name === "click") { + name = "contextmenu"; + delete modifiers.right; + } + } else if (modifiers.middle) { + if (dynamic) { + name = "(" + name + ")==='click'?'mouseup':(" + name + ")"; + } else if (name === "click") { + name = "mouseup"; + } + } + if (modifiers.capture) { + delete modifiers.capture; + name = prependModifierMarker("!", name, dynamic); + } + if (modifiers.once) { + delete modifiers.once; + name = prependModifierMarker("~", name, dynamic); + } + if (modifiers.passive) { + delete modifiers.passive; + name = prependModifierMarker("&", name, dynamic); + } + var events2; + if (modifiers.native) { + delete modifiers.native; + events2 = el.nativeEvents || (el.nativeEvents = {}); + } else { + events2 = el.events || (el.events = {}); + } + var newHandler = rangeSetItem({ value: value.trim(), dynamic }, range2); + if (modifiers !== emptyObject) { + newHandler.modifiers = modifiers; + } + var handlers = events2[name]; + if (Array.isArray(handlers)) { + important ? handlers.unshift(newHandler) : handlers.push(newHandler); + } else if (handlers) { + events2[name] = important ? [newHandler, handlers] : [handlers, newHandler]; + } else { + events2[name] = newHandler; + } + el.plain = false; + } + function getRawBindingAttr(el, name) { + return el.rawAttrsMap[":" + name] || el.rawAttrsMap["v-bind:" + name] || el.rawAttrsMap[name]; + } + function getBindingAttr(el, name, getStatic) { + var dynamicValue = getAndRemoveAttr(el, ":" + name) || getAndRemoveAttr(el, "v-bind:" + name); + if (dynamicValue != null) { + return parseFilters(dynamicValue); + } else if (getStatic !== false) { + var staticValue = getAndRemoveAttr(el, name); + if (staticValue != null) { + return JSON.stringify(staticValue); + } + } + } + function getAndRemoveAttr(el, name, removeFromMap) { + var val; + if ((val = el.attrsMap[name]) != null) { + var list = el.attrsList; + for (var i = 0, l = list.length; i < l; i++) { + if (list[i].name === name) { + list.splice(i, 1); + break; + } + } + } + if (removeFromMap) { + delete el.attrsMap[name]; + } + return val; + } + function getAndRemoveAttrByRegex(el, name) { + var list = el.attrsList; + for (var i = 0, l = list.length; i < l; i++) { + var attr = list[i]; + if (name.test(attr.name)) { + list.splice(i, 1); + return attr; + } + } + } + function rangeSetItem(item, range2) { + if (range2) { + if (range2.start != null) { + item.start = range2.start; + } + if (range2.end != null) { + item.end = range2.end; + } + } + return item; + } + function genComponentModel(el, value, modifiers) { + var ref2 = modifiers || {}; + var number3 = ref2.number; + var trim = ref2.trim; + var baseValueExpression = "$$v"; + var valueExpression = baseValueExpression; + if (trim) { + valueExpression = "(typeof " + baseValueExpression + " === 'string'? " + baseValueExpression + ".trim(): " + baseValueExpression + ")"; + } + if (number3) { + valueExpression = "_n(" + valueExpression + ")"; + } + var assignment = genAssignmentCode(value, valueExpression); + el.model = { + value: "(" + value + ")", + expression: JSON.stringify(value), + callback: "function (" + baseValueExpression + ") {" + assignment + "}" + }; + } + function genAssignmentCode(value, assignment) { + var res = parseModel(value); + if (res.key === null) { + return value + "=" + assignment; + } else { + return "$set(" + res.exp + ", " + res.key + ", " + assignment + ")"; + } + } + var len; + var str; + var chr; + var index$1; + var expressionPos; + var expressionEndPos; + function parseModel(val) { + val = val.trim(); + len = val.length; + if (val.indexOf("[") < 0 || val.lastIndexOf("]") < len - 1) { + index$1 = val.lastIndexOf("."); + if (index$1 > -1) { + return { + exp: val.slice(0, index$1), + key: '"' + val.slice(index$1 + 1) + '"' + }; + } else { + return { + exp: val, + key: null + }; + } + } + str = val; + index$1 = expressionPos = expressionEndPos = 0; + while (!eof()) { + chr = next(); + if (isStringStart(chr)) { + parseString(chr); + } else if (chr === 91) { + parseBracket(chr); + } + } + return { + exp: val.slice(0, expressionPos), + key: val.slice(expressionPos + 1, expressionEndPos) + }; + } + function next() { + return str.charCodeAt(++index$1); + } + function eof() { + return index$1 >= len; + } + function isStringStart(chr2) { + return chr2 === 34 || chr2 === 39; + } + function parseBracket(chr2) { + var inBracket = 1; + expressionPos = index$1; + while (!eof()) { + chr2 = next(); + if (isStringStart(chr2)) { + parseString(chr2); + continue; + } + if (chr2 === 91) { + inBracket++; + } + if (chr2 === 93) { + inBracket--; + } + if (inBracket === 0) { + expressionEndPos = index$1; + break; + } + } + } + function parseString(chr2) { + var stringQuote = chr2; + while (!eof()) { + chr2 = next(); + if (chr2 === stringQuote) { + break; + } + } + } + var warn$1; + var RANGE_TOKEN = "__r"; + var CHECKBOX_RADIO_TOKEN = "__c"; + function model(el, dir, _warn) { + warn$1 = _warn; + var value = dir.value; + var modifiers = dir.modifiers; + var tag = el.tag; + var type = el.attrsMap.type; + if (true) { + if (tag === "input" && type === "file") { + warn$1("<" + el.tag + ' v-model="' + value + '" type="file">:\nFile inputs are read only. Use a v-on:change listener instead.', el.rawAttrsMap["v-model"]); + } + } + if (el.component) { + genComponentModel(el, value, modifiers); + return false; + } else if (tag === "select") { + genSelect(el, value, modifiers); + } else if (tag === "input" && type === "checkbox") { + genCheckboxModel(el, value, modifiers); + } else if (tag === "input" && type === "radio") { + genRadioModel(el, value, modifiers); + } else if (tag === "input" || tag === "textarea") { + genDefaultModel(el, value, modifiers); + } else if (!config.isReservedTag(tag)) { + genComponentModel(el, value, modifiers); + return false; + } else if (true) { + warn$1("<" + el.tag + ' v-model="' + value + `">: v-model is not supported on this element type. If you are working with contenteditable, it's recommended to wrap a library dedicated for that purpose inside a custom component.`, el.rawAttrsMap["v-model"]); + } + return true; + } + function genCheckboxModel(el, value, modifiers) { + var number3 = modifiers && modifiers.number; + var valueBinding = getBindingAttr(el, "value") || "null"; + var trueValueBinding = getBindingAttr(el, "true-value") || "true"; + var falseValueBinding = getBindingAttr(el, "false-value") || "false"; + addProp(el, "checked", "Array.isArray(" + value + ")?_i(" + value + "," + valueBinding + ")>-1" + (trueValueBinding === "true" ? ":(" + value + ")" : ":_q(" + value + "," + trueValueBinding + ")")); + addHandler(el, "change", "var $$a=" + value + ",$$el=$event.target,$$c=$$el.checked?(" + trueValueBinding + "):(" + falseValueBinding + ");if(Array.isArray($$a)){var $$v=" + (number3 ? "_n(" + valueBinding + ")" : valueBinding) + ",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&(" + genAssignmentCode(value, "$$a.concat([$$v])") + ")}else{$$i>-1&&(" + genAssignmentCode(value, "$$a.slice(0,$$i).concat($$a.slice($$i+1))") + ")}}else{" + genAssignmentCode(value, "$$c") + "}", null, true); + } + function genRadioModel(el, value, modifiers) { + var number3 = modifiers && modifiers.number; + var valueBinding = getBindingAttr(el, "value") || "null"; + valueBinding = number3 ? "_n(" + valueBinding + ")" : valueBinding; + addProp(el, "checked", "_q(" + value + "," + valueBinding + ")"); + addHandler(el, "change", genAssignmentCode(value, valueBinding), null, true); + } + function genSelect(el, value, modifiers) { + var number3 = modifiers && modifiers.number; + var selectedVal = 'Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return ' + (number3 ? "_n(val)" : "val") + "})"; + var assignment = "$event.target.multiple ? $$selectedVal : $$selectedVal[0]"; + var code = "var $$selectedVal = " + selectedVal + ";"; + code = code + " " + genAssignmentCode(value, assignment); + addHandler(el, "change", code, null, true); + } + function genDefaultModel(el, value, modifiers) { + var type = el.attrsMap.type; + if (true) { + var value$1 = el.attrsMap["v-bind:value"] || el.attrsMap[":value"]; + var typeBinding = el.attrsMap["v-bind:type"] || el.attrsMap[":type"]; + if (value$1 && !typeBinding) { + var binding = el.attrsMap["v-bind:value"] ? "v-bind:value" : ":value"; + warn$1(binding + '="' + value$1 + '" conflicts with v-model on the same element because the latter already expands to a value binding internally', el.rawAttrsMap[binding]); + } + } + var ref2 = modifiers || {}; + var lazy = ref2.lazy; + var number3 = ref2.number; + var trim = ref2.trim; + var needCompositionGuard = !lazy && type !== "range"; + var event = lazy ? "change" : type === "range" ? RANGE_TOKEN : "input"; + var valueExpression = "$event.target.value"; + if (trim) { + valueExpression = "$event.target.value.trim()"; + } + if (number3) { + valueExpression = "_n(" + valueExpression + ")"; + } + var code = genAssignmentCode(value, valueExpression); + if (needCompositionGuard) { + code = "if($event.target.composing)return;" + code; + } + addProp(el, "value", "(" + value + ")"); + addHandler(el, event, code, null, true); + if (trim || number3) { + addHandler(el, "blur", "$forceUpdate()"); + } + } + function normalizeEvents(on2) { + if (isDef(on2[RANGE_TOKEN])) { + var event = isIE ? "change" : "input"; + on2[event] = [].concat(on2[RANGE_TOKEN], on2[event] || []); + delete on2[RANGE_TOKEN]; + } + if (isDef(on2[CHECKBOX_RADIO_TOKEN])) { + on2.change = [].concat(on2[CHECKBOX_RADIO_TOKEN], on2.change || []); + delete on2[CHECKBOX_RADIO_TOKEN]; + } + } + var target$1; + function createOnceHandler$1(event, handler, capture) { + var _target = target$1; + return function onceHandler() { + var res = handler.apply(null, arguments); + if (res !== null) { + remove$2(event, onceHandler, capture, _target); + } + }; + } + var useMicrotaskFix = isUsingMicroTask && !(isFF && Number(isFF[1]) <= 53); + function add$1(name, handler, capture, passive) { + if (useMicrotaskFix) { + var attachedTimestamp = currentFlushTimestamp; + var original = handler; + handler = original._wrapper = function(e) { + if (e.target === e.currentTarget || e.timeStamp >= attachedTimestamp || e.timeStamp <= 0 || e.target.ownerDocument !== document) { + return original.apply(this, arguments); + } + }; + } + target$1.addEventListener(name, handler, supportsPassive ? { capture, passive } : capture); + } + function remove$2(name, handler, capture, _target) { + (_target || target$1).removeEventListener(name, handler._wrapper || handler, capture); + } + function updateDOMListeners(oldVnode, vnode) { + if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { + return; + } + var on2 = vnode.data.on || {}; + var oldOn = oldVnode.data.on || {}; + target$1 = vnode.elm; + normalizeEvents(on2); + updateListeners(on2, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context); + target$1 = void 0; + } + var events = { + create: updateDOMListeners, + update: updateDOMListeners + }; + var svgContainer; + function updateDOMProps(oldVnode, vnode) { + if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) { + return; + } + var key, cur; + var elm = vnode.elm; + var oldProps = oldVnode.data.domProps || {}; + var props2 = vnode.data.domProps || {}; + if (isDef(props2.__ob__)) { + props2 = vnode.data.domProps = extend({}, props2); + } + for (key in oldProps) { + if (!(key in props2)) { + elm[key] = ""; + } + } + for (key in props2) { + cur = props2[key]; + if (key === "textContent" || key === "innerHTML") { + if (vnode.children) { + vnode.children.length = 0; + } + if (cur === oldProps[key]) { + continue; + } + if (elm.childNodes.length === 1) { + elm.removeChild(elm.childNodes[0]); + } + } + if (key === "value" && elm.tagName !== "PROGRESS") { + elm._value = cur; + var strCur = isUndef(cur) ? "" : String(cur); + if (shouldUpdateValue(elm, strCur)) { + elm.value = strCur; + } + } else if (key === "innerHTML" && isSVG(elm.tagName) && isUndef(elm.innerHTML)) { + svgContainer = svgContainer || document.createElement("div"); + svgContainer.innerHTML = "" + cur + ""; + var svg = svgContainer.firstChild; + while (elm.firstChild) { + elm.removeChild(elm.firstChild); + } + while (svg.firstChild) { + elm.appendChild(svg.firstChild); + } + } else if (cur !== oldProps[key]) { + try { + elm[key] = cur; + } catch (e) { + } + } + } + } + function shouldUpdateValue(elm, checkVal) { + return !elm.composing && (elm.tagName === "OPTION" || isNotInFocusAndDirty(elm, checkVal) || isDirtyWithModifiers(elm, checkVal)); + } + function isNotInFocusAndDirty(elm, checkVal) { + var notInFocus = true; + try { + notInFocus = document.activeElement !== elm; + } catch (e) { + } + return notInFocus && elm.value !== checkVal; + } + function isDirtyWithModifiers(elm, newVal) { + var value = elm.value; + var modifiers = elm._vModifiers; + if (isDef(modifiers)) { + if (modifiers.number) { + return toNumber(value) !== toNumber(newVal); + } + if (modifiers.trim) { + return value.trim() !== newVal.trim(); + } + } + return value !== newVal; + } + var domProps = { + create: updateDOMProps, + update: updateDOMProps + }; + var parseStyleText = cached(function(cssText) { + var res = {}; + var listDelimiter = /;(?![^(]*\))/g; + var propertyDelimiter = /:(.+)/; + cssText.split(listDelimiter).forEach(function(item) { + if (item) { + var tmp = item.split(propertyDelimiter); + tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim()); + } + }); + return res; + }); + function normalizeStyleData(data) { + var style2 = normalizeStyleBinding(data.style); + return data.staticStyle ? extend(data.staticStyle, style2) : style2; + } + function normalizeStyleBinding(bindingStyle) { + if (Array.isArray(bindingStyle)) { + return toObject(bindingStyle); + } + if (typeof bindingStyle === "string") { + return parseStyleText(bindingStyle); + } + return bindingStyle; + } + function getStyle(vnode, checkChild) { + var res = {}; + var styleData; + if (checkChild) { + var childNode = vnode; + while (childNode.componentInstance) { + childNode = childNode.componentInstance._vnode; + if (childNode && childNode.data && (styleData = normalizeStyleData(childNode.data))) { + extend(res, styleData); + } + } + } + if (styleData = normalizeStyleData(vnode.data)) { + extend(res, styleData); + } + var parentNode2 = vnode; + while (parentNode2 = parentNode2.parent) { + if (parentNode2.data && (styleData = normalizeStyleData(parentNode2.data))) { + extend(res, styleData); + } + } + return res; + } + var cssVarRE = /^--/; + var importantRE = /\s*!important$/; + var setProp = function(el, name, val) { + if (cssVarRE.test(name)) { + el.style.setProperty(name, val); + } else if (importantRE.test(val)) { + el.style.setProperty(hyphenate(name), val.replace(importantRE, ""), "important"); + } else { + var normalizedName = normalize(name); + if (Array.isArray(val)) { + for (var i = 0, len2 = val.length; i < len2; i++) { + el.style[normalizedName] = val[i]; + } + } else { + el.style[normalizedName] = val; + } + } + }; + var vendorNames = ["Webkit", "Moz", "ms"]; + var emptyStyle; + var normalize = cached(function(prop) { + emptyStyle = emptyStyle || document.createElement("div").style; + prop = camelize(prop); + if (prop !== "filter" && prop in emptyStyle) { + return prop; + } + var capName = prop.charAt(0).toUpperCase() + prop.slice(1); + for (var i = 0; i < vendorNames.length; i++) { + var name = vendorNames[i] + capName; + if (name in emptyStyle) { + return name; + } + } + }); + function updateStyle(oldVnode, vnode) { + var data = vnode.data; + var oldData = oldVnode.data; + if (isUndef(data.staticStyle) && isUndef(data.style) && isUndef(oldData.staticStyle) && isUndef(oldData.style)) { + return; + } + var cur, name; + var el = vnode.elm; + var oldStaticStyle = oldData.staticStyle; + var oldStyleBinding = oldData.normalizedStyle || oldData.style || {}; + var oldStyle = oldStaticStyle || oldStyleBinding; + var style2 = normalizeStyleBinding(vnode.data.style) || {}; + vnode.data.normalizedStyle = isDef(style2.__ob__) ? extend({}, style2) : style2; + var newStyle = getStyle(vnode, true); + for (name in oldStyle) { + if (isUndef(newStyle[name])) { + setProp(el, name, ""); + } + } + for (name in newStyle) { + cur = newStyle[name]; + if (cur !== oldStyle[name]) { + setProp(el, name, cur == null ? "" : cur); + } + } + } + var style = { + create: updateStyle, + update: updateStyle + }; + var whitespaceRE = /\s+/; + function addClass(el, cls) { + if (!cls || !(cls = cls.trim())) { + return; + } + if (el.classList) { + if (cls.indexOf(" ") > -1) { + cls.split(whitespaceRE).forEach(function(c) { + return el.classList.add(c); + }); + } else { + el.classList.add(cls); + } + } else { + var cur = " " + (el.getAttribute("class") || "") + " "; + if (cur.indexOf(" " + cls + " ") < 0) { + el.setAttribute("class", (cur + cls).trim()); + } + } + } + function removeClass(el, cls) { + if (!cls || !(cls = cls.trim())) { + return; + } + if (el.classList) { + if (cls.indexOf(" ") > -1) { + cls.split(whitespaceRE).forEach(function(c) { + return el.classList.remove(c); + }); + } else { + el.classList.remove(cls); + } + if (!el.classList.length) { + el.removeAttribute("class"); + } + } else { + var cur = " " + (el.getAttribute("class") || "") + " "; + var tar = " " + cls + " "; + while (cur.indexOf(tar) >= 0) { + cur = cur.replace(tar, " "); + } + cur = cur.trim(); + if (cur) { + el.setAttribute("class", cur); + } else { + el.removeAttribute("class"); + } + } + } + function resolveTransition(def$$1) { + if (!def$$1) { + return; + } + if (typeof def$$1 === "object") { + var res = {}; + if (def$$1.css !== false) { + extend(res, autoCssTransition(def$$1.name || "v")); + } + extend(res, def$$1); + return res; + } else if (typeof def$$1 === "string") { + return autoCssTransition(def$$1); + } + } + var autoCssTransition = cached(function(name) { + return { + enterClass: name + "-enter", + enterToClass: name + "-enter-to", + enterActiveClass: name + "-enter-active", + leaveClass: name + "-leave", + leaveToClass: name + "-leave-to", + leaveActiveClass: name + "-leave-active" + }; + }); + var hasTransition = inBrowser && !isIE9; + var TRANSITION = "transition"; + var ANIMATION = "animation"; + var transitionProp = "transition"; + var transitionEndEvent = "transitionend"; + var animationProp = "animation"; + var animationEndEvent = "animationend"; + if (hasTransition) { + if (window.ontransitionend === void 0 && window.onwebkittransitionend !== void 0) { + transitionProp = "WebkitTransition"; + transitionEndEvent = "webkitTransitionEnd"; + } + if (window.onanimationend === void 0 && window.onwebkitanimationend !== void 0) { + animationProp = "WebkitAnimation"; + animationEndEvent = "webkitAnimationEnd"; + } + } + var raf = inBrowser ? window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : setTimeout : function(fn) { + return fn(); + }; + function nextFrame(fn) { + raf(function() { + raf(fn); + }); + } + function addTransitionClass(el, cls) { + var transitionClasses = el._transitionClasses || (el._transitionClasses = []); + if (transitionClasses.indexOf(cls) < 0) { + transitionClasses.push(cls); + addClass(el, cls); + } + } + function removeTransitionClass(el, cls) { + if (el._transitionClasses) { + remove(el._transitionClasses, cls); + } + removeClass(el, cls); + } + function whenTransitionEnds(el, expectedType, cb) { + var ref2 = getTransitionInfo(el, expectedType); + var type = ref2.type; + var timeout = ref2.timeout; + var propCount = ref2.propCount; + if (!type) { + return cb(); + } + var event = type === TRANSITION ? transitionEndEvent : animationEndEvent; + var ended = 0; + var end = function() { + el.removeEventListener(event, onEnd); + cb(); + }; + var onEnd = function(e) { + if (e.target === el) { + if (++ended >= propCount) { + end(); + } + } + }; + setTimeout(function() { + if (ended < propCount) { + end(); + } + }, timeout + 1); + el.addEventListener(event, onEnd); + } + var transformRE = /\b(transform|all)(,|$)/; + function getTransitionInfo(el, expectedType) { + var styles = window.getComputedStyle(el); + var transitionDelays = (styles[transitionProp + "Delay"] || "").split(", "); + var transitionDurations = (styles[transitionProp + "Duration"] || "").split(", "); + var transitionTimeout = getTimeout(transitionDelays, transitionDurations); + var animationDelays = (styles[animationProp + "Delay"] || "").split(", "); + var animationDurations = (styles[animationProp + "Duration"] || "").split(", "); + var animationTimeout = getTimeout(animationDelays, animationDurations); + var type; + var timeout = 0; + var propCount = 0; + if (expectedType === TRANSITION) { + if (transitionTimeout > 0) { + type = TRANSITION; + timeout = transitionTimeout; + propCount = transitionDurations.length; + } + } else if (expectedType === ANIMATION) { + if (animationTimeout > 0) { + type = ANIMATION; + timeout = animationTimeout; + propCount = animationDurations.length; + } + } else { + timeout = Math.max(transitionTimeout, animationTimeout); + type = timeout > 0 ? transitionTimeout > animationTimeout ? TRANSITION : ANIMATION : null; + propCount = type ? type === TRANSITION ? transitionDurations.length : animationDurations.length : 0; + } + var hasTransform = type === TRANSITION && transformRE.test(styles[transitionProp + "Property"]); + return { + type, + timeout, + propCount, + hasTransform + }; + } + function getTimeout(delays, durations) { + while (delays.length < durations.length) { + delays = delays.concat(delays); + } + return Math.max.apply(null, durations.map(function(d, i) { + return toMs(d) + toMs(delays[i]); + })); + } + function toMs(s) { + return Number(s.slice(0, -1).replace(",", ".")) * 1e3; + } + function enter(vnode, toggleDisplay) { + var el = vnode.elm; + if (isDef(el._leaveCb)) { + el._leaveCb.cancelled = true; + el._leaveCb(); + } + var data = resolveTransition(vnode.data.transition); + if (isUndef(data)) { + return; + } + if (isDef(el._enterCb) || el.nodeType !== 1) { + return; + } + var css = data.css; + var type = data.type; + var enterClass = data.enterClass; + var enterToClass = data.enterToClass; + var enterActiveClass = data.enterActiveClass; + var appearClass = data.appearClass; + var appearToClass = data.appearToClass; + var appearActiveClass = data.appearActiveClass; + var beforeEnter = data.beforeEnter; + var enter2 = data.enter; + var afterEnter = data.afterEnter; + var enterCancelled = data.enterCancelled; + var beforeAppear = data.beforeAppear; + var appear = data.appear; + var afterAppear = data.afterAppear; + var appearCancelled = data.appearCancelled; + var duration = data.duration; + var context = activeInstance; + var transitionNode = activeInstance.$vnode; + while (transitionNode && transitionNode.parent) { + context = transitionNode.context; + transitionNode = transitionNode.parent; + } + var isAppear = !context._isMounted || !vnode.isRootInsert; + if (isAppear && !appear && appear !== "") { + return; + } + var startClass = isAppear && appearClass ? appearClass : enterClass; + var activeClass = isAppear && appearActiveClass ? appearActiveClass : enterActiveClass; + var toClass = isAppear && appearToClass ? appearToClass : enterToClass; + var beforeEnterHook = isAppear ? beforeAppear || beforeEnter : beforeEnter; + var enterHook = isAppear ? typeof appear === "function" ? appear : enter2 : enter2; + var afterEnterHook = isAppear ? afterAppear || afterEnter : afterEnter; + var enterCancelledHook = isAppear ? appearCancelled || enterCancelled : enterCancelled; + var explicitEnterDuration = toNumber(isObject(duration) ? duration.enter : duration); + if (explicitEnterDuration != null) { + checkDuration(explicitEnterDuration, "enter", vnode); + } + var expectsCSS = css !== false && !isIE9; + var userWantsControl = getHookArgumentsLength(enterHook); + var cb = el._enterCb = once(function() { + if (expectsCSS) { + removeTransitionClass(el, toClass); + removeTransitionClass(el, activeClass); + } + if (cb.cancelled) { + if (expectsCSS) { + removeTransitionClass(el, startClass); + } + enterCancelledHook && enterCancelledHook(el); + } else { + afterEnterHook && afterEnterHook(el); + } + el._enterCb = null; + }); + if (!vnode.data.show) { + mergeVNodeHook(vnode, "insert", function() { + var parent = el.parentNode; + var pendingNode = parent && parent._pending && parent._pending[vnode.key]; + if (pendingNode && pendingNode.tag === vnode.tag && pendingNode.elm._leaveCb) { + pendingNode.elm._leaveCb(); + } + enterHook && enterHook(el, cb); + }); + } + beforeEnterHook && beforeEnterHook(el); + if (expectsCSS) { + addTransitionClass(el, startClass); + addTransitionClass(el, activeClass); + nextFrame(function() { + removeTransitionClass(el, startClass); + if (!cb.cancelled) { + addTransitionClass(el, toClass); + if (!userWantsControl) { + if (isValidDuration(explicitEnterDuration)) { + setTimeout(cb, explicitEnterDuration); + } else { + whenTransitionEnds(el, type, cb); + } + } + } + }); + } + if (vnode.data.show) { + toggleDisplay && toggleDisplay(); + enterHook && enterHook(el, cb); + } + if (!expectsCSS && !userWantsControl) { + cb(); + } + } + function leave(vnode, rm) { + var el = vnode.elm; + if (isDef(el._enterCb)) { + el._enterCb.cancelled = true; + el._enterCb(); + } + var data = resolveTransition(vnode.data.transition); + if (isUndef(data) || el.nodeType !== 1) { + return rm(); + } + if (isDef(el._leaveCb)) { + return; + } + var css = data.css; + var type = data.type; + var leaveClass = data.leaveClass; + var leaveToClass = data.leaveToClass; + var leaveActiveClass = data.leaveActiveClass; + var beforeLeave = data.beforeLeave; + var leave2 = data.leave; + var afterLeave = data.afterLeave; + var leaveCancelled = data.leaveCancelled; + var delayLeave = data.delayLeave; + var duration = data.duration; + var expectsCSS = css !== false && !isIE9; + var userWantsControl = getHookArgumentsLength(leave2); + var explicitLeaveDuration = toNumber(isObject(duration) ? duration.leave : duration); + if (isDef(explicitLeaveDuration)) { + checkDuration(explicitLeaveDuration, "leave", vnode); + } + var cb = el._leaveCb = once(function() { + if (el.parentNode && el.parentNode._pending) { + el.parentNode._pending[vnode.key] = null; + } + if (expectsCSS) { + removeTransitionClass(el, leaveToClass); + removeTransitionClass(el, leaveActiveClass); + } + if (cb.cancelled) { + if (expectsCSS) { + removeTransitionClass(el, leaveClass); + } + leaveCancelled && leaveCancelled(el); + } else { + rm(); + afterLeave && afterLeave(el); + } + el._leaveCb = null; + }); + if (delayLeave) { + delayLeave(performLeave); + } else { + performLeave(); + } + function performLeave() { + if (cb.cancelled) { + return; + } + if (!vnode.data.show && el.parentNode) { + (el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] = vnode; + } + beforeLeave && beforeLeave(el); + if (expectsCSS) { + addTransitionClass(el, leaveClass); + addTransitionClass(el, leaveActiveClass); + nextFrame(function() { + removeTransitionClass(el, leaveClass); + if (!cb.cancelled) { + addTransitionClass(el, leaveToClass); + if (!userWantsControl) { + if (isValidDuration(explicitLeaveDuration)) { + setTimeout(cb, explicitLeaveDuration); + } else { + whenTransitionEnds(el, type, cb); + } + } + } + }); + } + leave2 && leave2(el, cb); + if (!expectsCSS && !userWantsControl) { + cb(); + } + } + } + function checkDuration(val, name, vnode) { + if (typeof val !== "number") { + warn(" explicit " + name + " duration is not a valid number - got " + JSON.stringify(val) + ".", vnode.context); + } else if (isNaN(val)) { + warn(" explicit " + name + " duration is NaN - the duration expression might be incorrect.", vnode.context); + } + } + function isValidDuration(val) { + return typeof val === "number" && !isNaN(val); + } + function getHookArgumentsLength(fn) { + if (isUndef(fn)) { + return false; + } + var invokerFns = fn.fns; + if (isDef(invokerFns)) { + return getHookArgumentsLength(Array.isArray(invokerFns) ? invokerFns[0] : invokerFns); + } else { + return (fn._length || fn.length) > 1; + } + } + function _enter(_, vnode) { + if (vnode.data.show !== true) { + enter(vnode); + } + } + var transition = inBrowser ? { + create: _enter, + activate: _enter, + remove: function remove$$1(vnode, rm) { + if (vnode.data.show !== true) { + leave(vnode, rm); + } else { + rm(); + } + } + } : {}; + var platformModules = [ + attrs, + klass, + events, + domProps, + style, + transition + ]; + var modules = platformModules.concat(baseModules); + var patch = createPatchFunction({ nodeOps, modules }); + if (isIE9) { + document.addEventListener("selectionchange", function() { + var el = document.activeElement; + if (el && el.vmodel) { + trigger(el, "input"); + } + }); + } + var directive = { + inserted: function inserted(el, binding, vnode, oldVnode) { + if (vnode.tag === "select") { + if (oldVnode.elm && !oldVnode.elm._vOptions) { + mergeVNodeHook(vnode, "postpatch", function() { + directive.componentUpdated(el, binding, vnode); + }); + } else { + setSelected(el, binding, vnode.context); + } + el._vOptions = [].map.call(el.options, getValue); + } else if (vnode.tag === "textarea" || isTextInputType(el.type)) { + el._vModifiers = binding.modifiers; + if (!binding.modifiers.lazy) { + el.addEventListener("compositionstart", onCompositionStart); + el.addEventListener("compositionend", onCompositionEnd); + el.addEventListener("change", onCompositionEnd); + if (isIE9) { + el.vmodel = true; + } + } + } + }, + componentUpdated: function componentUpdated(el, binding, vnode) { + if (vnode.tag === "select") { + setSelected(el, binding, vnode.context); + var prevOptions = el._vOptions; + var curOptions = el._vOptions = [].map.call(el.options, getValue); + if (curOptions.some(function(o, i) { + return !looseEqual(o, prevOptions[i]); + })) { + var needReset = el.multiple ? binding.value.some(function(v) { + return hasNoMatchingOption(v, curOptions); + }) : binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, curOptions); + if (needReset) { + trigger(el, "change"); + } + } + } + } + }; + function setSelected(el, binding, vm) { + actuallySetSelected(el, binding, vm); + if (isIE || isEdge) { + setTimeout(function() { + actuallySetSelected(el, binding, vm); + }, 0); + } + } + function actuallySetSelected(el, binding, vm) { + var value = binding.value; + var isMultiple = el.multiple; + if (isMultiple && !Array.isArray(value)) { + warn('