UNPKG

@litexa/core

Version:

Litexa, a programming language for writing Alexa skills

479 lines (401 loc) 14 kB
### # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ### randomIndex = (count) -> Math.floor Math.random() * count shuffleArray = (array) -> shuffled = ( a for a in array ) for i in [0...shuffled.length] j = i + Math.floor(Math.random() * (shuffled.length - i)) a = shuffled[i] b = shuffled[j] shuffled[i] = b shuffled[j] = a return shuffled randomArrayItem = (array) -> array[randomIndex(array.length)] diceRoll = (sides) -> # produce a number between 1 and sides, inclusive sides = sides ? 6 1 + Math.floor( Math.random() * sides ) diceCheck = (number, sides) -> diceRoll(sides) <= number escapeSpeech = (line) -> return "" unless line? return "" + line deepClone = (thing) -> JSON.parse( JSON.stringify(thing) ) isActuallyANumber = (data) -> not isNaN(parseInt(data)) pickSayString = (context, key, count) -> sayData = context.db.read('__sayHistory') ? [] history = sayData[key] ? [] value = 0 switch when count == 2 # with two, we can only toggle anyway if history[0]? value = 1 - history[0] else value = randomIndex(2) history[0] = value % 2 when count < 5 # until 4, the pattern below is a little # over constrained, producing a repeating # set rather than a random sequence, # so we only guarantee # no adjacent repetition instead value = randomIndex(count) if value == history[0] value = ( value + 1 ) % count history[0] = value % 5 else # otherwise, guarantee we'll see at least # half the remaining options before repeating # one, up to a capped history of 8, beyond which # it's likely too difficult to detect repetition. value = randomIndex(count) for i in [0...count] break unless value in history value = ( value + 1 ) % count history.unshift value cap = Math.min 8, count / 2 history = history[0...cap] sayData[key] = history context.db.write '__sayHistory', sayData return value % count pickSayFragment = (context, key, options) -> index = pickSayString context, key, options.length return options[index] exports.DataTablePrototype = pickRandomIndex: -> return randomIndex(@length) find: (key, value) -> idx = @keys[key] return null unless idx? for row in [0...length] if @[row][idx] == value return row return null exports.Logging = log: -> console.log.apply( null, arguments ) error: -> console.error.apply( null, arguments ) minutesBetween = (before, now) -> return 999999 unless before? and now? Math.floor( Math.abs(now - before) / (60 * 1000) ) hoursBetween = (before, now) -> return 999999 unless before? and now? Math.floor( Math.abs(now - before) / (60 * 60 * 1000) ) daysBetween = (before, now) -> return 999999 unless before? and now? now = (new Date(now)).setHours(0, 0, 0, 0) before = (new Date(before)).setHours(0, 0, 0, 0) Math.floor( Math.abs(now - before) / ( 24 * 60 * 60 * 1000 ) ) Math.clamp = (min, max, x) -> Math.min( Math.max( min, x ), max ) rgbFromHex = (hex) -> return [0,0,0] unless hex?.length? hex = hex[2..] if hex.indexOf('0x') == 0 hex = hex[1..] if hex.indexOf('#') == 0 return switch hex.length when 3 read = (v) -> v = parseInt(v, 16) return v + 16 * v [ read(hex[0]), read(hex[1]), read(hex[2]) ] when 6 [ parseInt(hex[0..1],16), parseInt(hex[2..3],16), parseInt(hex[4..5],16) ] else [0,0,0] hexFromRGB = (rgb) -> r = Math.clamp( 0, 255, Math.floor(rgb[0]) ).toString(16) g = Math.clamp( 0, 255, Math.floor(rgb[1]) ).toString(16) b = Math.clamp( 0, 255, Math.floor(rgb[2]) ).toString(16) r = "0" + r if r.length < 2 g = "0" + g if g.length < 2 b = "0" + b if b.length < 2 r + g + b rgbFromHSL = (hsl) -> h = ( hsl[0] % 360 + 360 ) % 360 s = Math.clamp( 0.0, 1.0, hsl[1] ) l = Math.clamp( 0.0, 1.0, hsl[2] ) h /= 60.0 c = (1.0 - Math.abs(2.0 * l - 1.0)) * s x = c * (1.0 - Math.abs(h % 2.0 - 1.0)) m = l - 0.5 * c c += m x += m m = Math.floor( m * 255 ) c = Math.floor( c * 255 ) x = Math.floor( x * 255 ) switch Math.floor(h) when 0 then return [c,x,m] when 1 then return [x,c,m] when 2 then return [m,c,x] when 3 then return [m,x,c] when 4 then return [x,m,c] else return [c,m,x] brightenColor = (c, percent) -> isHex = false unless Array.isArray(c) c = rgbFromHex(c) isHex = true c = interpolateRGB c, [255,255,255], percent / 100.0 if isHex return hexFromRGB(c) return c interpolateRGB = (c1, c2, l) -> [r, g, b] = c1 r += (c2[0] - r) * l g += (c2[1] - g) * l b += (c2[2] - b) * l [r.toFixed(0), g.toFixed(0), b.toFixed(0)] reportValueMetric = ( metricType, value, unit ) -> params = MetricData: [] Namespace: 'Litexa' params.MetricData.push { MetricName: metricType Dimensions: [ { Name: 'project' Value: litexa.projectName } ], StorageResolution: 60 Timestamp: new Date().toISOString() Unit: unit ? 'None' Value: value ? 1 } #console.log "reporting metric #{JSON.stringify(params)}" return unless cloudWatch? cloudWatch.putMetricData params, (err, data) -> if err? console.error( "Cloudwatch metrics write fail #{err}" ) litexa.extensions = postProcessors: [] extendedEvents: {} load: (location, name) -> # during testing, this might already be in the shared context, skip it if so if name of litexa.extensions #console.log ("skipping extension load, already loaded") return testing = if litexa.localTesting then "(test mode)" else "" #console.log "loading extension #{location}/#{name} #{testing}" fullPath = "#{litexa.modulesRoot}/#{location}/#{name}/litexa.extension" lib = litexa.extensions[name] = require fullPath if lib.loadPostProcessor? handler = lib.loadPostProcessor(litexa.localTesting) if handler? #console.log "installing post processor for extension #{name}" handler.extensionName = name litexa.extensions.postProcessors.push handler if lib.events? for k, v of lib.events(false) #console.log "registering extended event #{k}" litexa.extensions.extendedEvents[k] = v finishedLoading: -> # sort the postProcessors by their actions processors = litexa.extensions.postProcessors count = processors.length # identify dependencies for a in processors a.dependencies = [] continue unless a.consumesTags? for tag in a.consumesTags for b in processors when b != a continue unless b.producesTags? if tag in b.producesTags a.dependencies.push b ready = ( a for a in processors when a.dependencies.length == 0 ) processors = ( p for p in processors when p.dependencies.length > 0 ) sorted = [] for guard in [0...count] break if ready.length == 0 node = ready.pop() for p in processors p.dependencies = ( pp for pp in p.dependencies when pp != node ) if p.dependencies.length == 0 ready.push p processors = ( p for p in processors when p.dependencies.length > 0 ) sorted.push node unless sorted.length == count throw new Error "Failed to sort postprocessors by dependency" litexa.extensions.postProcessors = sorted class DBTypeWrapper constructor: (@db, @language) -> @cache = {} read: (name) -> if name of @cache return @cache[name] dbType = __languages[@language].dbTypes[name] value = @db.read name if dbType?.prototype? # if this is a typed variable, and it appears # the type is a constructible, e.g. a Class if value? # patch the prototype if it exists Object.setPrototypeOf value, dbType.prototype else # or construct a new instance value = new dbType @db.write name, value else if dbType?.Prepare? # otherwise if it's typed and it provides a # wrapping Prepare function unless value? if dbType.Initialize? # optionally invoke an initialize value = dbType.Initialize() else # otherwise assume we start from an # empty object value = {} @db.write name, value # wrap the cached object, whatever it is # the function wants to return. Note it's # still the input value object that gets saved # to the database either way! value = dbType.Prepare(value) @cache[name] = value return value write: (name, value) -> # clear out the cache on any writes delete @cache[name] dbType = __languages[@language].dbTypes[name] if dbType? # for typed objects, we can only replace with # another object, OR clear out the object and # let initialization happen again on the next # read, whenever that happens if not value? @db.write name, null else if typeof(value) == 'object' @db.write name, value else throw new Error "@#{name} is a typed variable, you can only assign an object or null to it." else @db.write name, value finalize: (cb) -> @db.finalize cb # Monetization inSkillProductBought = (stateContext, referenceName) -> isp = await getProductByReferenceName(stateContext, referenceName) return (isp?.entitled == 'ENTITLED') getProductByReferenceName = (stateContext, referenceName) -> if stateContext.monetization.fetchEntitlements await fetchEntitlements stateContext for p in stateContext.monetization.inSkillProducts if p.referenceName == referenceName return p return null getProductByProductId = (stateContext, productId) -> if stateContext.monetization.fetchEntitlements await fetchEntitlements stateContext for p in stateContext.monetization.inSkillProducts if p.productId == productId return p return null buildBuyInSkillProductDirective = (stateContext, referenceName) -> isp = await getProductByReferenceName stateContext, referenceName unless isp? console.log "buildBuyInSkillProductDirective(): in-skill product \"#{referenceName}\" not found." return stateContext.directives.push { "type": "Connections.SendRequest" "name": "Buy" "payload": { "InSkillProduct": { "productId": isp.productId } } "token": "bearer " + stateContext.event.context.System.apiAccessToken } stateContext.shouldEndSession = true fetchEntitlements = (stateContext, ignoreCache = false) -> if !stateContext.monetization.fetchEntitlements and !ignoreCache return Promise.resolve() new Promise (resolve, reject) -> try https = require('https') catch console.log "skipping fetchEntitlements, no https present" reject() unless stateContext.event.context.System.apiEndpoint # If there's no API endpoint this is an offline test. resolve() # endpoint is region-specific: # e.g. https://api.amazonalexa.com vs. https://api.eu.amazonalexa.com apiEndpoint = stateContext.event.context.System.apiEndpoint apiEndpoint = apiEndpoint.replace("https://", "") apiPath = "/v1/users/~current/skills/~current/inSkillProducts" token = "bearer " + stateContext.event.context.System.apiAccessToken options = host: apiEndpoint path: apiPath method: 'GET' headers: "Content-Type": 'application/json' "Accept-Language": stateContext.request.locale "Authorization": token req = https.get options, (res) => res.setEncoding("utf8") if res.statusCode != 200 reject() returnData = "" res.on 'data', (chunk) => returnData += chunk res.on 'end', () => console.log("fetchEntitlements() returned: #{returnData}") stateContext.monetization.inSkillProducts = JSON.parse(returnData).inSkillProducts ? [] stateContext.monetization.fetchEntitlements = false stateContext.db.write "__monetization", stateContext.monetization resolve() req.on 'error', (e) -> console.log "Error while querying inSkillProducts: #{e}" reject(e) getReferenceNameByProductId = (stateContext, productId) -> for p in stateContext.monetization.inSkillProducts if p.productId == productId return p.referenceName return null buildCancelInSkillProductDirective = (stateContext, referenceName) => isp = await getProductByReferenceName(stateContext, referenceName) unless isp? console.log "buildCancelInSkillProductDirective(): in-skill product \"#{referenceName}\" not found." return stateContext.directives.push { "type": "Connections.SendRequest" "name": "Cancel" "payload": { "InSkillProduct": { "productId": isp.productId } } "token": "bearer " + stateContext.event.context.System.apiAccessToken } stateContext.shouldEndSession = true buildUpsellInSkillProductDirective = (stateContext, referenceName, upsellMessage = '') => isp = await getProductByReferenceName(stateContext, referenceName) unless isp? console.log "buildUpsellInSkillProductDirective(): in-skill product \"#{referenceName}\" not found." return stateContext.directives.push { "type": "Connections.SendRequest" "name": "Upsell" "payload": { "InSkillProduct": { "productId": isp.productId } "upsellMessage": upsellMessage } "token": "bearer " + stateContext.event.context.System.apiAccessToken } stateContext.shouldEndSession = true