Added tweens.misc.AngleTween

Added tweens.misc.AngleTween

require "LunoPunk.Tween"

export ^

-- Tweens from one angle to another.
class AngleTween extends Tween
	-- Constructor.
	-- @param	complete	Optional completion callback.
	-- @param	type		Tween type.
	 new: (complete, type) =>
		@__start, @__range = 0, 0
		-- The current value.
		@angle = 0
		super 0, type, complete

	-- Tweens the value from one angle to another.
	-- @param	fromAngle		Start angle.
	-- @param	toAngle			End angle.
	-- @param	duration		Duration of the tween.
	-- @param	ease			Optional easer function.
	 tween: (fromAngle, toAngle, duration, ease = null) =>
		@__start, @angle = fromAngle, fromAngle
		d = toAngle - @angle
		a = math.abs(d)
		if a > 181
			@__range = (360 - a) * (if d > 0 then -1 else 1)
		else if a < 179
			@__range = d
		else
			@__range = LP.choose {180, -180}
		@__target = duration
		@__ease = ease
		@start!

	--  @private Updates the Tween.
	 update: =>
		super!
		@angle = (@__start + @__range * @t) % 360
		@angle += 360 if angle < 0


github: no highlights

github: no highlights

Former-commit-id: 0da3876bccb735098d256959752ea29d9d5f7ce7 [formerly 6ba8919bd28805ccb44bccdaafba1d04b9b3a236]
Former-commit-id: 7f2b0423f323999c12d1ebfa4504a88ff330640c

util = require 'util'

hex_to_char = (x) ->
  string.char(tonumber(x, 16))

unescape = (url) ->
  url\gsub("%%(%x%x)", hex_to_char)

handlers = {
  push: (repo, destination, json) ->
    branch = util.bold(json.ref\gsub 'refs/heads/', '')
    for i, c in pairs(json.commits)
      if i > 3
        break
      message = c.message\gsub '\n.*', ''
      ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{branch}, #{util.nonickalertword c.author.username}: #{message} #{c.url}"
    if #json.commits > 3
      ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{branch}, #{#json.commits-3} more commits not displayed."
  watch: (repo, destination, json) ->
    action = json.action
    if action == 'started'
      ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{util.nonickalertword json.sender.login}: +1 ★ (Total: #{json.repository.stargazers_count})"
    else
      ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{util.nonickalertword json.sender.login}: #{action} watching"
  ping: (repo, destination, json) ->
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: ping"
  commit_comment: (repo, destination, json) ->
    --commenter = json.comment.user.login
    body = json.comment.body
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{util.nonickalertword json.sender.login}: commented #{body}"
  pull_request: (repo, destination, json) ->
    action = json.action
    number = util.bold json.number
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{util.nonickalertword json.sender.login}: #{action} PR ##{number}: #{json.pull_request.title} #{json.pull_request.html_url}"
  issues: (repo, destination, json) ->
    action = json.action
    nr = util.bold json.issue.number
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{util.nonickalertword json.sender.login}: #{action} issue ##{nr}: #{json.issue.title} #{json.issue.html_url}"
  issue_comment: (repo, destination, json) ->
    action = json.action
    nr = util.bold json.issue.number
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{util.nonickalertword json.sender.login}: comment on issue ##{nr}: #{json.comment.body} #{json.issue.html_url}"
  fork: (repo, destination, json) ->
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{util.nonickalertword json.sender.login}: forked to #{json.forkee.html_url}"
  create: (repo, destination, json) ->
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{util.nonickalertword json.sender.login}: created #{json.ref_type} #{util.bold json.ref}"
  delete: (repo, destination, json) ->
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{util.nonickalertword json.sender.login}: deleted #{json.ref_type} #{util.bold json.ref}"
  gollum: (repo, destination, json) ->
    return
  release: (repo, destination, json) ->
    return
}

ivar2.webserver.regUrl '/github/(.*)', (req, res) ->
  destination = req.url\match('/github/(.+)/?$')
  destination = unescape(destination)
  json = util.json.decode(req.body)

  repo = json.repository.full_name

  event = req.headers['X-GitHub-Event']
  handler = handlers[event]
  if handler
    handler(repo, destination, json)
  else
    ivar2\Log('error', 'Unknown github event: %s', event)

  res\set_status(200)
  res\set_header('Content-Type', 'text/plain')
  res\set_body('ok')
  res\send()

return {}

util = require 'util'

hex_to_char = (x) ->
  string.char(tonumber(x, 16))

unescape = (url) ->
  url\gsub("%%(%x%x)", hex_to_char)

handlers = {
  push: (repo, destination, json) ->
    branch = util.bold(json.ref\gsub 'refs/heads/', '')
    for i, c in pairs(json.commits)
      if i > 3
        break
      message = c.message\gsub '\n.*', ''
      ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{branch}, #{c.author.username}: #{message} #{c.url}"
    if #json.commits > 3
      ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{branch}, #{#json.commits-3} more commits not displayed."
  watch: (repo, destination, json) ->
    action = json.action
    if action == 'started'
      ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{json.sender.login}: +1 ★ (Total: #{json.repository.stargazers_count})"
    else
      ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{json.sender.login}: #{action} watching"
  ping: (repo, destination, json) ->
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: ping"
  commit_comment: (repo, destination, json) ->
    --commenter = json.comment.user.login
    body = json.body
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{json.sender.login}: commented #{body}"
  pull_request: (repo, destination, json) ->
    action = json.action
    number = util.bold json.number
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{json.sender.login}: #{action} PR ##{number}: #{json.pull_request.title} #{json.pull_request.html_url}"
  issues: (repo, destination, json) ->
    action = json.action
    nr = util.bold json.issue.number
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{json.sender.login}: #{action} issue ##{nr}: #{json.issue.title} #{json.issue.html_url}"
  issue_comment: (repo, destination, json) ->
    action = json.action
    nr = util.bold json.issue.number
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{json.sender.login}: comment on issue ##{nr}: #{json.comment.body} #{json.issue.html_url}"
  fork: (repo, destination, json) ->
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{json.sender.login}: forked to #{json.forkee.html_url}"
  create: (repo, destination, json) ->
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{json.sender.login}: created #{json.ref_type} #{util.bold json.ref}"
  delete: (repo, destination, json) ->
    ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{json.sender.login}: deleted #{json.ref_type} #{util.bold json.ref}"
  gollum: (repo, destination, json) ->
    return
  release: (repo, destination, json) ->
    return
}

ivar2.webserver.regUrl '/github/(.*)', (req, res) ->
  destination = req.url\match('/github/(.+)/?$')
  destination = unescape(destination)
  json = util.json.decode(req.body)

  repo = json.repository.full_name

  event = req.headers['X-GitHub-Event']
  handler = handlers[event]
  if handler
    handler(repo, destination, json)
  else
    ivar2\Log('error', 'Unknown github event: %s', event)

  res\set_status(200)
  res\set_header('Content-Type', 'text/plain')
  res\set_body('ok')
  res\send()

return {}


logger: log the bot's own messages

logger: log the bot's own messages

Former-commit-id: 97b3ac21a980c4d8669ff361a08d4d67db60ad99 [formerly 003cd21f6e5c27a80f804bf1c95f7d83a5331a20]
Former-commit-id: a373523277d087f32aba9a84d52274d5af87702d

dbi = require 'DBI'
require'logging.console'
iconv = require'iconv'
iso2utf = iconv.new('UTF-8'.."//TRANSLIT", 'ISO-8859-1')
log = logging.console()

conn = false

schemas = {[[
  CREATE TABLE IF NOT EXISTS log (
    time timestamp DEFAULT now(),
    nick text,
    channel text,
    message text,
    type text
  );]],
  [[CREATE INDEX idx_time ON log(time);]],
  [[CREATE INDEX idx_channel ON log(channel);]]
}

toutf = (s) ->
  iso2utf\iconv(s)

connect = ->
  conn, err = DBI.Connect('PostgreSQL', ivar2.config.dbname, ivar2.config.dbuser, ivar2.config.dbpass, ivar2.config.dbhost, ivar2.config.dbport)
  unless conn
    log\error "Unable to connect to DB: #{err}"
    return

  conn\autocommit(true)

  --for s in *schemas
  --  a,b = DBI.Do(conn, s)

dbh = ->
  connect! unless conn

  -- Check if connection is alive
  alive = conn\ping!
  connect! unless alive

  success, err = DBI.Do(conn, 'SELECT NOW()')
  unless success
    log\error "SQL Connection :#{err}"
    connect!

  return conn

dblog = (type, source, destination, arg) =>
  nick = source.nick

  if type == 'PRIVMSG'
    -- action
    if arg\sub(1,1) == '\001' and arg\sub(-1) == '\001'
      arg = arg\sub(9, -2)
      type = 'ACTION'

  unless arg
    arg = ''

  insert = ->
    ins = dbh!\prepare('INSERT INTO log(nick,channel,message,type) values(?,?,?,?)')
    ins\execute(nick, destination, arg, type)

  -- First try to insert statement directly
  -- if that doesn't work; convert it from iso to utf and try again
  stmt, err = insert!
  unless stmt
    arg, err = toutf(arg)
    stmt, err = insert!
    unless stmt
      log\error err

history = (source, destination, nr) ->
  nr = tonumber(nr) or 1
  -- TODO ignore messages that are commands
  stmt = dbh!\prepare [[
    SELECT
      *
      FROM log
      WHERE channel=?
      AND (type = 'PRIVMSG' OR type = 'ACTION')
      ORDER BY time
      DESC LIMIT ?
    ]]
  stmt\execute destination, nr
  out = {}
  for row in stmt\rows(true) -- true for column names
    out[#out+1] = row

  if #out == 1
    return out[1].message
  return out

lastlog = (source, destination, arg) ->
  nr = tonumber(nr) or 1
  arg = '%'..arg..'%'
  stmt = dbh!\prepare [[
    SELECT
      *
      FROM log
      WHERE channel=?
      AND (type = 'PRIVMSG' OR type = 'ACTION')
      AND message LIKE ?
      ORDER BY time DESC
      LIMIT 20
    ]]
  stmt\execute destination, arg
  out = {}
  for row in stmt\rows(true) -- true for column names
    out[#out+1] = row

  if #out == 1
    return out[1].message
  return out

seen = (source, destination, nick) =>
  nick = ivar2.util.trim(nick)
  stmt, err = dbh!\prepare [[
    SELECT
        *,
        date_trunc('second', time) as sectime,
        date_trunc('second', age(now(), date_trunc('second', time))) as ago
      FROM log
      WHERE nick = ?
      ORDER BY time DESC
      LIMIT 1
    ]]
  unless stmt
    print(err)
  stmt\execute nick
  out = {}
  for row in stmt\rows(true) -- true for column names
    out[#out+1] = row

  actions =
    'ACTION': 'talking in third person'
    'TOPIC': 'changing topic'
    'PRIVMSG': 'jabbering'
    'NICK': 'changing nick'
    'QUIT': 'quitting'
    'PART': 'leaving'
    'NOTICE': 'sending a notice'
    'MODE': 'changing a mode'
    'JOIN': 'joining'

  if #out == 1
    r = out[1]
    message = ''
    if #r.message
      message == " with message #{r.message}"
    say "#{ivar2.util.bold r.nick} was last observed #{ivar2.util.italic actions[r.type]}#{message} #{r.ago} ago (#{r.sectime} UTC)"
  else
    say "#{ivar2.util.bold nick} was not seen in my lifetime."

return {
  PRIVMSG: {
    '^%pseen (.+)$': seen
    '^%plast$': (source, destination) =>
      say history(source,destination,1)
    '^%plastlog (.+)$': (source, destination, arg) =>
      out = {}
      for k,v in *lastlog(source, destination, arg)
        -- Skip commands
        unless k.message\match('^%p')
          table.insert out, string.format('<%s> %s', k.nick, k.message)
      say table.concat(out, ' ')
    (s, d, a) =>
      dblog @, 'PRIVMSG', s, d, a
  }
  PRIVMSG_OUT: {
    (s, d, a) =>
      dblog @, 'PRIVMSG', s, d, a
  }
  NOTICE_OUT: {
    (s, d, a) =>
      dblog @, 'NOTICE', s, d, a
  }
  NOTICE: {
    (s, d, a) =>
      dblog @, 'NOTICE', s, d, a
  }
  JOIN: {
    (s, d, a) =>
      dblog @, 'JOIN', s, d, a
  }
  PART: {
    (s, d, a) =>
      dblog @, 'PART', s, d, a
  }
  KICK: {
    (s, d, a) =>
      dblog @, 'KICK', s, d, a
  }
  MODE: {
    (s, d, a) =>
      dblog @, 'MODE', s, d, a
  }
  TOPIC: {
    (s, d, a) =>
      dblog @, 'TOPIC', s, d, a
  }
  NICK: {
    (s, d, a) =>
      dblog @, 'NICK', s, d, a
  }
  QUIT: {
    (s, d, a) =>
      dblog @, 'QUIT', s, d, a
  }
}

dbi = require 'DBI'
require'logging.console'
iconv = require'iconv'
iso2utf = iconv.new('UTF-8'.."//TRANSLIT", 'ISO-8859-1')
log = logging.console()

conn = false

schemas = {[[
  CREATE TABLE IF NOT EXISTS log (
    time timestamp DEFAULT now(),
    nick text,
    channel text,
    message text,
    type text
  );]],
  [[CREATE INDEX idx_time ON log(time);]],
  [[CREATE INDEX idx_channel ON log(channel);]]
}

toutf = (s) ->
  iso2utf\iconv(s)

connect = ->
  conn, err = DBI.Connect('PostgreSQL', ivar2.config.dbname, ivar2.config.dbuser, ivar2.config.dbpass, ivar2.config.dbhost, ivar2.config.dbport)
  unless conn
    log\error "Unable to connect to DB: #{err}"
    return

  conn\autocommit(true)

  --for s in *schemas
  --  a,b = DBI.Do(conn, s)

dbh = ->
  connect! unless conn

  -- Check if connection is alive
  alive = conn\ping!
  connect! unless alive

  success, err = DBI.Do(conn, 'SELECT NOW()')
  unless success
    log\error "SQL Connection :#{err}"
    connect!

  return conn

dblog = (type, source, destination, arg) =>
  nick = source.nick

  if type == 'PRIVMSG'
    -- action
    if arg\sub(1,1) == '\001' and arg\sub(-1) == '\001'
      arg = arg\sub(9, -2)
      type = 'ACTION'

  unless arg
    arg = ''

  insert = ->
    ins = dbh!\prepare('INSERT INTO log(nick,channel,message,type) values(?,?,?,?)')
    ins\execute(nick, destination, arg, type)

  -- First try to insert statement directly
  -- if that doesn't work; convert it from iso to utf and try again
  stmt, err = insert!
  unless stmt
    arg, err = toutf(arg)
    stmt, err = insert!
    unless stmt
      log\error err

history = (source, destination, nr) ->
  nr = tonumber(nr) or 1
  -- TODO ignore messages that are commands
  stmt = dbh!\prepare [[
    SELECT
      *
      FROM log
      WHERE channel=?
      AND (type = 'PRIVMSG' OR type = 'ACTION')
      ORDER BY time
      DESC LIMIT ?
    ]]
  stmt\execute destination, nr
  out = {}
  for row in stmt\rows(true) -- true for column names
    out[#out+1] = row

  if #out == 1
    return out[1].message
  return out

lastlog = (source, destination, arg) ->
  nr = tonumber(nr) or 1
  arg = '%'..arg..'%'
  stmt = dbh!\prepare [[
    SELECT
      *
      FROM log
      WHERE channel=?
      AND (type = 'PRIVMSG' OR type = 'ACTION')
      AND message LIKE ?
      ORDER BY time DESC
      LIMIT 20
    ]]
  stmt\execute destination, arg
  out = {}
  for row in stmt\rows(true) -- true for column names
    out[#out+1] = row

  if #out == 1
    return out[1].message
  return out

seen = (source, destination, nick) =>
  nick = ivar2.util.trim(nick)
  stmt, err = dbh!\prepare [[
    SELECT
        *,
        date_trunc('second', time) as sectime,
        date_trunc('second', age(now(), date_trunc('second', time))) as ago
      FROM log
      WHERE nick = ?
      ORDER BY time DESC
      LIMIT 1
    ]]
  unless stmt
    print(err)
  stmt\execute nick
  out = {}
  for row in stmt\rows(true) -- true for column names
    out[#out+1] = row

  actions =
    'ACTION': 'talking in third person'
    'TOPIC': 'changing topic'
    'PRIVMSG': 'jabbering'
    'NICK': 'changing nick'
    'QUIT': 'quitting'
    'PART': 'leaving'
    'NOTICE': 'sending a notice'
    'MODE': 'changing a mode'
    'JOIN': 'joining'

  if #out == 1
    r = out[1]
    message = ''
    if #r.message
      message == " with message #{r.message}"
    say "#{ivar2.util.bold r.nick} was last observed #{ivar2.util.italic actions[r.type]}#{message} #{r.ago} ago (#{r.sectime} UTC)"
  else
    say "#{ivar2.util.bold nick} was not seen in my lifetime."

return {
  PRIVMSG: {
    '^%pseen (.+)$': seen
    '^%plast$': (source, destination) =>
      say history(source,destination,1)
    '^%plastlog (.+)$': (source, destination, arg) =>
      out = {}
      for k,v in *lastlog(source, destination, arg)
        -- Skip commands
        unless k.message\match('^%p')
          table.insert out, string.format('<%s> %s', k.nick, k.message)
      say table.concat(out, ' ')
    (s, d, a) =>
      dblog @, 'PRIVMSG', s, d, a
  }
  NOTICE: {
    (s, d, a) =>
      dblog @, 'NOTICE', s, d, a
  }
  JOIN: {
    (s, d, a) =>
      dblog @, 'JOIN', s, d, a
  }
  PART: {
    (s, d, a) =>
      dblog @, 'PART', s, d, a
  }
  KICK: {
    (s, d, a) =>
      dblog @, 'KICK', s, d, a
  }
  MODE: {
    (s, d, a) =>
      dblog @, 'MODE', s, d, a
  }
  TOPIC: {
    (s, d, a) =>
      dblog @, 'TOPIC', s, d, a
  }
  NICK: {
    (s, d, a) =>
      dblog @, 'NICK', s, d, a
  }
  QUIT: {
    (s, d, a) =>
      dblog @, 'QUIT', s, d, a
  }
}


stonks: new memes

stonks: new memes

util = require'util'
simplehttp = util.simplehttp
json = util.json
urlEncode = util.urlEncode

quote = (a, withname, withoutprice) ->
  if not withname
    withname = false
  if not withoutprice
    withoutprice = false
  data = simplehttp "https://query1.finance.yahoo.com/v10/finance/quoteSummary/#{urlEncode a}?modules=price"
  data = json.decode(data)
  res = data['quoteSummary']['result'][1]
  price = res.price

  out = {}

  c =  price.currency
  if price.currencySymbol
    c = price.currencySymbol
  unless withoutprice
    out[#out+1] = c

  current_price = price.regularMarketPrice.fmt
  current_per = price.regularMarketChangePercent.fmt
  current_raw_per = price.regularMarketChangePercent.raw
  regularmarket = true
  premarket = false
  postmarket = false
  if price.preMarketTime
    if price.preMarketTime > price.regularMarketTime
      current_price = price.preMarketPrice.fmt
      current_per = price.preMarketChangePercent.fmt
      current_raw_per = price.preMarketChangePercent.raw
      premarket = true
    elseif price.postMarketTime and price.postMarketTime > price.regularMarketTime
      current_price = price.postMarketPrice.fmt
      current_per = price.postMarketChangePercent.fmt
      current_raw_per = price.postMarketChangePercent.raw
      postmarket = true

  unless withoutprice
    out[#out+1] = current_price
  if current_raw_per > 0
    current_per = util.green current_per
  else
    current_per = util.red current_per
  out[#out+1] = "("..current_per ..")"

  --if price.preMarketPrice.fmt
  --  out[#out+1] = "("..price.preMarketPrice.fmt..")"

  --if price.postMarketPrice.fmt
  --  out[#out+1] = "("..price.postMarketPrice.fmt..")"
  --
  --

  if withname
    if price.longName == json.null
      out[#out+1] = price.shortName
    else
      out[#out+1] = price.longName

    if premarket or postmarket
      out[#out+1] = "🥱"

  return table.concat(out, ' ')

sayquote = (s, d, a) =>
  say quote(a, true)

meme = (s, d, a) =>
  stonks = {'GME', 'AMC', 'BTC-USD', 'TSLA'}
  out = {}
  for stonk in *stonks
    out[#out+1] = stonk .. ' ' .. quote(stonk)

  say table.concat(out, ' ')

idx = (s, d, a) =>
  stonks = {'OSEBX.OL', '^IXIC', '^GSPC', '^DJI', '^NYA', '^N225'}
  out = {}
  for stonk in *stonks
    out[#out+1] = stonk .. ' ' .. quote(stonk, true, true)

  say table.concat(out, ' ')

PRIVMSG:
  '^%pq (.*)$': sayquote
  '^%pstonks': meme
  '^%pidx$': idx

util = require'util'
simplehttp = util.simplehttp
json = util.json
urlEncode = util.urlEncode

quote = (a, withname, withoutprice) ->
  if not withname
    withname = false
  if not withoutprice
    withoutprice = false
  data = simplehttp "https://query1.finance.yahoo.com/v10/finance/quoteSummary/#{urlEncode a}?modules=price"
  data = json.decode(data)
  res = data['quoteSummary']['result'][1]
  price = res.price

  out = {}

  c =  price.currency
  if price.currencySymbol
    c = price.currencySymbol
  unless withoutprice
    out[#out+1] = c

  current_price = price.regularMarketPrice.fmt
  current_per = price.regularMarketChangePercent.fmt
  current_raw_per = price.regularMarketChangePercent.raw
  regularmarket = true
  premarket = false
  postmarket = false
  if price.preMarketTime
    if price.preMarketTime > price.regularMarketTime
      current_price = price.preMarketPrice.fmt
      current_per = price.preMarketChangePercent.fmt
      current_raw_per = price.preMarketChangePercent.raw
      premarket = true
    elseif price.postMarketTime and price.postMarketTime > price.regularMarketTime
      current_price = price.postMarketPrice.fmt
      current_per = price.postMarketChangePercent.fmt
      current_raw_per = price.postMarketChangePercent.raw
      postmarket = true

  unless withoutprice
    out[#out+1] = current_price
  if current_raw_per > 0
    current_per = util.green current_per
  else
    current_per = util.red current_per
  out[#out+1] = "("..current_per ..")"

  --if price.preMarketPrice.fmt
  --  out[#out+1] = "("..price.preMarketPrice.fmt..")"

  --if price.postMarketPrice.fmt
  --  out[#out+1] = "("..price.postMarketPrice.fmt..")"
  --
  --

  if withname
    if price.longName == json.null
      out[#out+1] = price.shortName
    else
      out[#out+1] = price.longName

    if premarket or postmarket
      out[#out+1] = "🥱"

  return table.concat(out, ' ')

sayquote = (s, d, a) =>
  say quote(a, true)

meme = (s, d, a) =>
  stonks = {'GME', 'AMC', 'BB', 'NOK'}
  out = {}
  for stonk in *stonks
    out[#out+1] = stonk .. ' ' .. quote(stonk)

  say table.concat(out, ' ')

idx = (s, d, a) =>
  stonks = {'OSEBX.OL', '^IXIC', '^GSPC', '^DJI', '^NYA', '^N225'}
  out = {}
  for stonk in *stonks
    out[#out+1] = stonk .. ' ' .. quote(stonk, true, true)

  say table.concat(out, ' ')

PRIVMSG:
  '^%pq (.*)$': sayquote
  '^%pstonks': meme
  '^%pidx$': idx


Partial implementation of Scene based on HaxePunk

require "LovePunk.Tweener"

export ^

class Scene extends Tweener
	new: =>
		super!
		@camera = nil
		@visible = true
		-- private
		@__typeFirst = nil
		@__count = nil
		@__farthest = nil
		@__first = nil
		@__layerFarthest = nil
		@__layerNearest = nil
		@__layers = nil
		@__add = {}
		@__remove = {}
		@__recycle = {}
		@__nearest = nil
		@__sprite = nil
		@__uniqueTypes = nil
		@__updatedFirst = nil

	-- Override this; called when Scene is switch to, and set to the currently active scene.
	begin: =>

	-- Override this; called when Scene is changed, and the active scene is no longer this.
	-- Changed from end because of issues with the name "end" in Lua
	endScene: =>

	-- Override this, called when game gains focus
	focusGained: =>

	-- Override this, called when game loses focus
	focusLost: =>

	-- Performed by the game loop, updates all contained Entities.
	-- If you override this to give your Scene update code, remember
	-- to call super.update() or your Entities will not be updated.
	update: =>
		e = @__updatedFirst
		while e != nil
			if e.active
				e\updateTweens! if e\hasTween!
				e\update!
			e.graphic\update! if e.graphic != nil and e.graphic.active
			e = e.__updateNext

	-- Performed by the game loop, renders all contained Entities.
	-- If you override this to give your Scene render code, remember
	-- to call super.render() or your Entities will not be rendered.
	render: =>
		if @__layerSort
			@__layerList\sort @__layerSort if #@__layerList > 1

		if LP.renderMode == RenderMode.Hardware
			clearSprites!
			-- AtlasData.setScene(@)

		for layer in *@__layerList do
			e = @__renderLast\get layer
			while e != nil
				e\render! if e.visable
				e = e.__renderPrev

		-- AtlasData\render! if LP.renderMode == RenderMode.Hardware

	-- X position of the mouse in the Scene.
	mouseX: => LP.screen.mouseX + @camera.x

	-- Y position of the mouse in the Scene.
	mouseY: => LP.screen.mouseY + @camera.y

	-- Sprite used to store layer sprites when RenderMode.HARDWARE is set.
	sprite: => @__sprite

	-- Adds the Entity to the Scene at the end of the frame.
	-- @param	e		Entity object you want to add.
	-- @return	The added Entity object.
	add: (e) =>
		@__add[#@__add] = e
		e

	-- Removes the Entity from the Scene at the end of the frame.
	-- @param	e		Entity object you want to remove.
	-- @return	The removed Entity object.
	remove: (e) =>
		@__remove[#@__remove] = e
		e

	-- Removes all Entities from the Scene at the end of the frame.
	removeAll: =>
		e = @__updatedFirst
		while e != nil
			@__remove[#@__remove] = e
			e = e.__updateNext

	-- Adds multiple Entities to the scene.
	-- @param	...list		Several Entities (as arguments) or an Array/Vector of Entities.
	addList: (list) =>
		for e in *list do @\add e

	-- Removes multiple Entities from the scene.
	-- @param	...list		Several Entities (as arguments) or an Array/Vector of Entities.
	removeList: (list) =>
		for e in *list do @\remove e

	-- Adds an Entity to the Scene with the Graphic object.
	-- @param	graphic		Graphic to assign the Entity.
	-- @param	x			X position of the Entity.
	-- @param	y			Y position of the Entity.
	-- @param	layer		Layer of the Entity.
	-- @return	The Entity that was added.
	addGraphic: (graphic, layer = LP.BASELAYER, x = 0, y = 0) =>
		e = Entity x, y, graphic

		e.layer = layer if layer != LP.BASELAYER
		e.active = false

		@\add e

	-- Adds an Entity to the Scene with the Mask object.
	-- @param	mask	Mask to assign the Entity.
	-- @param	type	Collision type of the Entity.
	-- @param	x		X position of the Entity.
	-- @param	y		Y position of the Entity.
	-- @return	The Entity that was added.
	addMask: (mask, type, x = 0, y = 0) =>
		e = Entity x, y, nil, mask

		e.type = type if type != ""
		e.active, e.visable = false, false

		@\add e

	-- Returns a new Entity, or a stored recycled Entity if one exists.
	-- @param	classType			The Class of the Entity you want to add.
	-- @param	addToScene			Add it to the Scene immediately.
	-- @param	constructorsArgs	List of the entity constructor arguments (optional).
	-- @return	The new Entity object.
	create: (classType, addToScene = true, ...) =>
		className = classType.__class.__name
		e = @__recycle[className]
		if e != nil
			@__recycle[className] = e.__recycleNext
			e.__recycleNext = nil
		else
			e = classType ...

		return @\add e if addToScene

		e

	-- Removes the Entity from the Scene at the end of the frame and recycles it.
	-- The recycled Entity can then be fetched again by calling the create() function.
	-- @param	e		The Entity to recycle.
	-- @return	The recycled Entity.
	recycle: (e) =>
		@__recycle[#@__recycle] = e
		@\remove e

	-- Clears stored reycled Entities of the Class type.
	-- @param	classType		The Class type to clear.
	clearRecycled: (classType) =>
		idx = 0
		for i = 1, #@__recycle do
			if @__recycle[i].__class == classType
				idx = i
				break

		e = @__recycle[idx]
		n = nil
		while e != nil
			n = e.__recycleNext
			e.__recycleNext = nil
			e = n
		table.remove @__recycle, idx
		return

	-- Clears stored recycled Entities of all Class types.
	clearRecycledAll: =>
		for e in *@__recycle do
			@\clearRecycled e.__class

	bringToFront: (e) =>
	sendToBack: (e) =>
	bringForward: (e) =>
	sendBackward: (e) =>
	isAtFront: (e) =>
	isAtBack: (e) =>
	collideRect: (type, rX, rY, rWidth, rHeight) =>
	collidePoint: (type, pX, pY) =>
	collideLine: (type, fromX, fromY, toX, toY, precision, p) =>
	collideRectInto: (type, rX, rY, rWidth, rHeight, into) =>
	collideCircleInto: (type, circleX, circleY, radius, into) =>
	collidePointInt: (type, pX, pY, into) =>
	nearestToRect: (type, x, y, width, height) =>
	nearestToEntity: (type, e, useHitboxes) =>
	nearestToClass: (type, e, classType, useHitboxes) =>
	nearestToPoint: (type, x, y, useHitboxes) =>
	count: => @__count --TODO
	typeCount: (type) =>
	classCount: (c) =>
	layerCount: (layer) =>
	first: => @__updatedFirst --TODO
	layers: => @__layers --TODO
	typeFirst: (type) =>
	classFirst: (c) =>
	layerFirst: (layer) =>
	layerLast: (layer) =>
	getSpriteByLayer: (layer) =>
	getFarthest: => @__farthest --TODO
	getNearest: => @__nearest --TODO
	getLayerFarthest: => @__layerFarthest --TODO
	getLayerNearest: => @__layerNearest --TODO
	getUniqueTypes: => @__uniqueTypes --TODO
	getType: (type, into) =>
	getClass: (c, into) =>
	getLayer: (layer, into) =>
	getAll: (into) =>
	getInstance: (name) =>
	updateLists: (shouldAdd) =>

require "LovePunk.Tweener"

export ^

class Scene extends Tweener
	new: =>
		super!
		@camera = nil
		@visible = true
		-- private
		@__typeFirst = nil
		@__count = nil
		@__farthest = nil
		@__first = nil
		@__layerFarthest = nil
		@__layerNearest = nil
		@__layers = nil
		@__mouseX = nil
		@__mouseY = nil
		@__nearest = nil
		@__sprite = nil
		@__uniqueTypes = nil
		@__updatedFirst = nil

	begin: =>
	-- Changed from end because of issues with the name "end" in Lua
	endScene: =>
	focusGained: =>
	focusLost: =>

	update: =>
	render: =>

	mouseX: => @__mouseX
	mouseY: => @__mouseY
	sprite: => @__sprite

	add: (e) =>
	remove: (e) =>
	removeAll: =>
	addList: (list) =>
	removeList: =>
	addGraphic: (graphic, layer, x, y) =>
	addMask: (mask, type, x, y) =>
	create: (classType, addToScene) =>
	recycle: (e) =>
	clearRecycled: (classType) =>
	clearRecycledAll: =>
	bringToFront: (e) =>
	sendToBack: (e) =>
	bringForward: (e) =>
	sendBackward: (e) =>
	isAtFront: (e) =>
	isAtBack: (e) =>
	collideRect: (type, rX, rY, rWidth, rHeight) =>
	collidePoint: (type, pX, pY) =>
	collideLine: (type, fromX, fromY, toX, toY, precision, p) =>
	collideRectInto: (type, rX, rY, rWidth, rHeight, into) =>
	collideCircleInto: (type, circleX, circleY, radius, into) =>
	collidePointInt: (type, pX, pY, into) =>
	nearestToRect: (type, x, y, width, height) =>
	nearestToEntity: (type, e, useHitboxes) =>
	nearestToClass: (type, e, classType, useHitboxes) =>
	nearestToPoint: (type, x, y, useHitboxes) =>
	count: => @__count --TODO
	typeCount: (type) =>
	classCount: (c) =>
	layerCount: (layer) =>
	first: => @__updatedFirst --TODO
	layers: => @__layers --TODO
	typeFirst: (type) =>
	classFirst: (c) =>
	layerFirst: (layer) =>
	layerLast: (layer) =>
	getSpriteByLayer: (layer) =>
	getFarthest: => @__farthest --TODO
	getNearest: => @__nearest --TODO
	getLayerFarthest: => @__layerFarthest --TODO
	getLayerNearest: => @__layerNearest --TODO
	getUniqueTypes: => @__uniqueTypes --TODO
	getType: (type, into) =>
	getClass: (c, into) =>
	getLayer: (layer, into) =>
	getAll: (into) =>
	getInstance: (name) =>
	updateLists: (shouldAdd) =>


Location of TextureBlob in require was wrong.

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- Copyright 2016 [bk]door.maus

glurg = require "glurg"
Promise = require "glurg.common.Promise"
TextureBlob = require "glurg.resources.TextureBlob"

class Texture
	new: (blob) =>
		Promise.keep(blob, Promise.IsClass(blob, TextureBlob))
		Promise.keep("not blob.is_data", not blob.is_data)

		@_texture = glurg.resources.Texture.new(blob._blob)
		@blob = blob

	save_png: (filename) =>
		Promise.keep("filename", Promise.IsString(filename))

		decodedPixelBuffer = glurg.common.DataBuffer.new!
		outputImagePixelBuffer = glurg.common.DataBuffer.new!

		@_texture\decode_image(decodedPixelBuffer)
		glurg.common.pixel_data_buffer_to_png(decodedPixelBuffer,
			@blob.width, @blob.height, 4,
			outputImagePixelBuffer)

		fileStream = glurg.common.SimpleFileStream.new!
		fileStream\open(filename, glurg.common.file_mode_write)
		fileStream\write(outputImagePixelBuffer.data,
			outputImagePixelBuffer.size)
		fileStream\close!

return Texture

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- Copyright 2016 [bk]door.maus

glurg = require "glurg"
Promise = require "glurg.common.Promise"
TextureBlob = require "glurg.common.TextureBlob"

class Texture
	new: (blob) =>
		Promise.keep(blob, Promise.IsClass(blob, TextureBlob))
		Promise.keep("not blob.is_data", not blob.is_data)

		@_texture = glurg.resources.Texture.new(blob._blob)
		@blob = blob

	save_png: (filename) =>
		Promise.keep("filename", Promise.IsString(filename))

		decodedPixelBuffer = glurg.common.DataBuffer.new!
		outputImagePixelBuffer = glurg.common.DataBuffer.new!

		@_texture\decode_image(decodedPixelBuffer)
		glurg.common.pixel_data_buffer_to_png(decodedPixelBuffer,
			@blob.width, @blob.height, 4,
			outputImagePixelBuffer)

		fileStream = glurg.common.SimpleFileStream.new!
		fileStream\open(filename, glurg.common.file_mode_write)
		fileStream\write(outputImagePixelBuffer.data,
			outputImagePixelBuffer.size)
		fileStream\close!

return Texture