github: prevent "1 more commit not displayed"

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.*', ''
      if i == 1
        message = message .. ' ' .. json.compare
      ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{branch}, #{util.nonickalertword c.author.username}: #{message}"
    if #json.commits >= 4
      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.*', ''
      if i == 1
        message = message .. ' ' .. json.compare
      ivar2\Msg 'privmsg', destination, nil, "[#{repo}]: #{branch}, #{util.nonickalertword c.author.username}: #{message}"
    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 {}


Updates to Tween

Updates to Tween

* Added comments
* Made target and time private
* Removed @percent and made it a function accessor

export ^

TweenType = {
	"Persist"
	"Looping"
	"OneShot"
}

class Tween
	-- Constructor. Specify basic information about the Tween.
	-- @param	duration		Duration of the tween (in seconds or frames).
	-- @param	type			Tween type, one of Tween.PERSIST (default), Tween.LOOPING, or Tween.ONESHOT.
	-- @param	complete		Optional callback for when the Tween completes.
	-- @param	ease			Optional easer function to apply to the Tweened value.
	new: (duration, type = TweenType.Persist, complete = nil, ease = nil) =>
		assert duration != nil, "Cannot create a Tween without a duration"
		@active = false
		@__scale = 0.0
		@__parent = nil
		@__finish = false

		@__target = duration
		@type = if type == nil then "Persist" else type
		@ease = ease
		@t = 0
		@__time = 0

		--if complete != nil
			-- TODO: Add event for when tween is finished
			-- addEventListener(TweenEvent.FINISH, complete);

	-- Updates the Tween, called by World.
	update: =>
		@__time += if LP.fixed then 1 else LP.elapsed
		@t = @__time/@__target
		if @ease != nil and @t > 0 and @t < 1
			@t = @ease @t

		if @__time >= @__target
			@t = 1
			@__finish = true

		-- dispatchEvent(new TweenEvent(TweenEvent.UPDATE));
		return

	-- Starts the Tween, or restarts it if it's currently running.
	start: =>
		@__time = 0
		if @__target == 0
			@active = false
			-- dispatchEvent(new TweenEvent(TweenEvent.FINISH));
		else
			@active = true
			-- dispatchEvent(new TweenEvent(TweenEvent.START));
		return

	-- @private Called when the Tween completes.
	finish = =>
		switch @type
			when "Persist"
				@__time = @__target
				@active = false
			when "Looping"
				@__time %= @__target
				@t = @__time/@__target
				if @ease != nil and @t > 0 and @t < 1
					@t = @ease @t
				@start!
			when "OneShot"
				@__time = @__target
				@active = false
				-- parent.removeTween(this);
		@__finish = false
		-- dispatchEvent(new TweenEvent(TweenEvent.FINISH));

	-- Immediately stops the Tween and removes it from its Tweener without calling the complete callback.
	cancel: =>
		@active = false
		@parent.removeTween @ if @parent != nil
		return

	percent: (value) =>
		return @__time/@__target if value == nil
		@__time = @__target * value
		@__time

	scale: =>
		@__scale

export ^

TweenType = {
	"Persist"
	"Looping"
	"OneShot"
}

class Tween
	new: (duration, type = TweenType.Persist, complete = nil, ease = nil) =>
		assert duration != nil, "Cannot create a Tween without a duration"
		@active = false
		@percent = 0.0
		@__scale = 0.0
		@__parent = nil
		@__finish = false

		@target = duration
		@type = if type == nil then "Persist" else type
		@ease = ease
		@t = 0

		--if complete != nil
			-- TODO: Add event for when tween is finished
			-- addEventListener(TweenEvent.FINISH, complete);

	scale: =>
		@__scale

	update: =>
		@time += if LP.fixed then 1 else LP.elapsed
		@t = @time/@target
		if @ease != nil and @t > 0 and @t < 1
			@t = @ease @t

		if @time >= @target
			@t = 1
			@__finish = true

		-- dispatchEvent(new TweenEvent(TweenEvent.UPDATE));
		return

	start: =>
		@time = 0
		if @target == 0
			@active = false
			-- dispatchEvent(new TweenEvent(TweenEvent.FINISH));
		else
			@active = true
			-- dispatchEvent(new TweenEvent(TweenEvent.START));
		return

	finish = =>
		switch @type
			when "Persist"
				@time = @target
				@active = false
			when "Looping"
				@time %= @target
				@t = @time/@target
				if @ease != nil and @t > 0 and @t < 1
					@t = @ease @t
				@start!
			when "OneShot"
				@time = @target
				@active = false
				-- parent.removeTween(this);
		@__finish = false
		-- dispatchEvent(new TweenEvent(TweenEvent.FINISH));

	cancel: =>
		@active = false
		@parent.removeTween @ if @parent != nil
		return


I think this is what Tweener is supposed to be for parent\removeTween

require "LovePunk.utils.EventListener"
require "LovePunk.tweens.TweenEvent"

export ^

TweenType = {
	"Persist"
	"Looping"
	"OneShot"
}

class Tween
	-- Constructor. Specify basic information about the Tween.
	-- @param	duration		Duration of the tween (in seconds or frames).
	-- @param	type			Tween type, one of Tween.PERSIST (default), Tween.LOOPING, or Tween.ONESHOT.
	-- @param	complete		Optional callback for when the Tween completes.
	-- @param	ease			Optional easer function to apply to the Tweened value.
	new: (duration, type = TweenType.Persist, complete = nil, ease = nil) =>
		assert duration != nil, "Cannot create a Tween without a duration"
		@active = false
		@__scale = 0.0
		@__parent = nil
		@__finish = false

		@__target = duration
		@type = if type == nil then "Persist" else type
		@ease = ease
		@t = 0
		@__time = 0

		if complete != nil
			-- TODO: Add event for when tween is finished
			AddEventListener TweenEvent.FINISH!, complete

	-- Updates the Tween, called by World.
	update: =>
		@__time += if LP.fixed then 1 else LP.elapsed
		@t = @__time/@__target
		if @ease != nil and @t > 0 and @t < 1
			@t = @ease @t

		if @__time >= @__target
			@t = 1
			@__finish = true

		DispatchEvent TweenEvent TweenEvent.UPDATE!
		return

	-- Starts the Tween, or restarts it if it's currently running.
	start: =>
		@__time = 0
		if @__target == 0
			@active = false
			DispatchEvent TweenEvent TweenEvent.FINISH!
		else
			@active = true
			DispatchEvent TweenEvent TweenEvent.START!
		return

	-- @private Called when the Tween completes.
	finish = =>
		switch @type
			when "Persist"
				@__time = @__target
				@active = false
			when "Looping"
				@__time %= @__target
				@t = @__time/@__target
				if @ease != nil and @t > 0 and @t < 1
					@t = @ease @t
				@start!
			when "OneShot"
				@__time = @__target
				@active = false
				@parent\removeTween @
		@__finish = false
		DispatchEvent TweenEvent TweenEvent.FINISH!

	-- Immediately stops the Tween and removes it from its Tweener without calling the complete callback.
	cancel: =>
		@active = false
		@parent\removeTween @ if @parent != nil
		return

	percent: (value) =>
		return @__time/@__target if value == nil
		@__time = @__target * value
		@__time

	scale: =>
		@__scale

require "LovePunk.utils.EventListener"
require "LovePunk.tweens.TweenEvent"

export ^

TweenType = {
	"Persist"
	"Looping"
	"OneShot"
}

class Tween
	-- Constructor. Specify basic information about the Tween.
	-- @param	duration		Duration of the tween (in seconds or frames).
	-- @param	type			Tween type, one of Tween.PERSIST (default), Tween.LOOPING, or Tween.ONESHOT.
	-- @param	complete		Optional callback for when the Tween completes.
	-- @param	ease			Optional easer function to apply to the Tweened value.
	new: (duration, type = TweenType.Persist, complete = nil, ease = nil) =>
		assert duration != nil, "Cannot create a Tween without a duration"
		@active = false
		@__scale = 0.0
		@__parent = nil
		@__finish = false

		@__target = duration
		@type = if type == nil then "Persist" else type
		@ease = ease
		@t = 0
		@__time = 0

		if complete != nil
			-- TODO: Add event for when tween is finished
			AddEventListener TweenEvent.FINISH!, complete

	-- Updates the Tween, called by World.
	update: =>
		@__time += if LP.fixed then 1 else LP.elapsed
		@t = @__time/@__target
		if @ease != nil and @t > 0 and @t < 1
			@t = @ease @t

		if @__time >= @__target
			@t = 1
			@__finish = true

		DispatchEvent TweenEvent TweenEvent.UPDATE!
		return

	-- Starts the Tween, or restarts it if it's currently running.
	start: =>
		@__time = 0
		if @__target == 0
			@active = false
			DispatchEvent TweenEvent TweenEvent.FINISH!
		else
			@active = true
			DispatchEvent TweenEvent TweenEvent.START!
		return

	-- @private Called when the Tween completes.
	finish = =>
		switch @type
			when "Persist"
				@__time = @__target
				@active = false
			when "Looping"
				@__time %= @__target
				@t = @__time/@__target
				if @ease != nil and @t > 0 and @t < 1
					@t = @ease @t
				@start!
			when "OneShot"
				@__time = @__target
				@active = false
				-- parent.removeTween(this);
		@__finish = false
		DispatchEvent TweenEvent TweenEvent.FINISH!

	-- Immediately stops the Tween and removes it from its Tweener without calling the complete callback.
	cancel: =>
		@active = false
		@parent.removeTween @ if @parent != nil
		return

	percent: (value) =>
		return @__time/@__target if value == nil
		@__time = @__target * value
		@__time

	scale: =>
		@__scale


Well, I forgot MoonScript had string interpolation.

rex = require "rex_pcre"
cli = require "cliargs"
lfs = require "lfs"

import insert from table

contains = (t, item) ->
	return true if item == nil
	for k, v in pairs t
		return true if item == v
	false

file_ext = (str) ->
	return "." .. str\match(".([^%.]+)$")

local dir_list, dir_listing
dir_listing = (path, file) ->
	f = path .. '/' .. file
	attr = lfs.attributes(f).mode
	{file, attr, if attr == 'directory' then dir_list(f)}

dir_list = (path) ->
	[dir_listing(path, file) for file in lfs.dir(path) when not contains({'.', '..'}, file)]

map_listing = (structure, func) ->
	for k, v in pairs structure
		func(v, map_listing)

print_listing = (value, func) ->
	{file, mode, data} = value
	print file, mode
	func(data, print_listing) if data

dbg = (b) -> return "[DBG: #{(b and 'Y') or 'N'}]"

perform_action = (old, new, action = "hardlink") ->
	print dbg(debug), switch action
		when "copy"
			"[cp]: Not yet implemented."
		when "move"
			"[mv]: Not yet implemented."
		when "symlink", "hardlink"
			lfs.link(old, new, action == "hardlink" or false) if not debug
			"[link]: #{old} to: #{new}"
		else
			"unknown action."

process_files = (filedata, regex, parent) ->
	for k, {file, mode, data} in pairs filedata
		if rex.gsub(file, regex, '')
			if mode == 'directory'
				f = rex.gsub(file, regex, '')\match("^%s*(.-)%s*$")
				f = parent.out .. '/' .. f
				lfs.mkdir(f)
				process_files(data, regex, {in: parent.in .. '/' .. file, out: f})
			else
				ext = file_ext(file)
				newer_f = rex.gsub(file, regex, '')
				new_f = rex.match(newer_f, [[(.*)(v\d)]]) or newer_f
				new_f = new_f\gsub('[ ]', '')
				new_f = rex.gsub(new_f, [[([^\d]*)(\d+)[^\d]*$]], [[%1_ep%2]], 1)
				old_f, new_f = parent.in .. '/' .. file, parent.out .. '/' .. new_f .. ext
				perform_action(old_f, new_f, args["a"] or nil)

		else
			print "ERR: ", file, mode

cli\set_name("regexulon.lua")
cli\add_argument("DIR", "directory to scan")
cli\add_argument("TARGET", "target directory")
cli\optarg("IGNORED", "ignored files/directories")
cli\add_flag("-a, --action", "action to take: cp, mv, symlink, hardlink, defaults to hardlink", "hardlink")
cli\add_flag("-v, --version", "prints the program version")
cli\add_flag("-d, --debug", "script will simulate excution, print actions.")
export args = cli\parse_args()
return if not args

ignored_files = switch type(args["IGNORED"])
	when "string"
		{}
	else
		args["IGNORED"]

inputdir = args["DIR"]
targetdir = args["TARGET"]
export debug = args["d"]

files = dir_list(args["DIR"])
regex = [==[\(.*?\)|\[.*?\]]==]

map_listing(files, print_listing)
processed = process_files(files, regex, {in: inputdir, out: targetdir})

rex = require "rex_pcre"
cli = require "cliargs"
lfs = require "lfs"

import insert from table

contains = (t, item) ->
	return true if item == nil
	for k, v in pairs t
		return true if item == v
	false

file_ext = (str) ->
	return "." .. str\match(".([^%.]+)$")

local dir_list, dir_listing
dir_listing = (path, file) ->
	f = path .. '/' .. file
	attr = lfs.attributes(f).mode
	{file, attr, if attr == 'directory' then dir_list(f)}

dir_list = (path) ->
	[dir_listing(path, file) for file in lfs.dir(path) when not contains({'.', '..'}, file)]

map_listing = (structure, func) ->
	for k, v in pairs structure
		func(v, map_listing)

print_listing = (value, func) ->
	{file, mode, data} = value
	print file, mode
	func(data, print_listing) if data

dbg = (b) -> return "[DBG:" .. ((b and "Y") or "N") .. "]"

perform_action = (old, new, action = "hardlink") ->
	print dbg(debug), switch action
		when "copy"
			"[cp]: Not yet implemented."
		when "move"
			"[mv]: Not yet implemented."
		when "symlink", "hardlink"
			lfs.link(old, new, action == "hardlink" or false) if not debug
			"[link]: " .. old .. "to: " .. new
		else
			"unknown action."

process_files = (filedata, regex, parent) ->
	for k, {file, mode, data} in pairs filedata
		if rex.gsub(file, regex, '')
			if mode == 'directory'
				f = rex.gsub(file, regex, '')\match("^%s*(.-)%s*$")
				f = parent.out .. '/' .. f
				lfs.mkdir(f)
				process_files(data, regex, {in: parent.in .. '/' .. file, out: f})
			else
				ext = file_ext(file)
				newer_f = rex.gsub(file, regex, '')
				new_f = rex.match(newer_f, [[(.*)(v\d)]]) or newer_f
				new_f = new_f\gsub('[ ]', '')
				new_f = rex.gsub(new_f, [[([^\d]*)(\d+)[^\d]*$]], [[%1_ep%2]], 1)
				old_f, new_f = parent.in .. '/' .. file, parent.out .. '/' .. new_f .. ext
				perform_action(old_f, new_f, args["a"] or nil)

		else
			print "ERR: ", file, mode

cli\set_name("regexulon.lua")
cli\add_argument("DIR", "directory to scan")
cli\add_argument("TARGET", "target directory")
cli\optarg("IGNORED", "ignored files/directories")
cli\add_flag("-a, --action", "action to take: cp, mv, symlink, hardlink, defaults to hardlink", "hardlink")
cli\add_flag("-v, --version", "prints the program version")
cli\add_flag("-d, --debug", "script will simulate excution, print actions.")
export args = cli\parse_args()
return if not args

ignored_files = switch type(args["IGNORED"])
	when "string"
		{}
	else
		args["IGNORED"]

inputdir = args["DIR"]
targetdir = args["TARGET"]
export debug = args["d"]

files = dir_list(args["DIR"])
regex = [==[\(.*?\)|\[.*?\]]==]

map_listing(files, print_listing)
processed = process_files(files, regex, {in: inputdir, out: targetdir})


update schema

update schema

db = require "lapis.nginx.postgres"
schema = require "lapis.db.schema"
migrations = require "lapis.db.migrations"

import types, create_table, create_index, drop_table from schema

make_schema = ->
  {
    :serial
    :varchar
    :text
    :time
    :integer
    :foreign_key
    :boolean
  } = schema.types

  -- Users
  create_table "users", {
    {"id", serial}
    {"token", "VARCHAR(6) NOT NULL"}
    {"email", text}
    {"name", text}
    {"created_at", time}
    {"updated_at", time}

    "PRIMARY KEY (id)"
  }

  create_index "users", "email", unique: true
  create_index "users", "token", unique: true

  -- Albums
  create_table "albums", {
    {"id", serial}
    {"user_id", foreign_key}
    {"token", "VARCHAR(6) NOT NULL"}
    {"title", text}
    {"metadata", "hstore"}
    {"views", integer}
    {"created_at", time}
    {"updated_at", time}

    "PRIMARY KEY (id)"
  }

  create_index "albums", "user_id"
  create_index "albums", "token", unique: true

  -- Images
  create_table "images", {
    {"id", serial}
    {"user_id", foreign_key}
    {"album_id", foreign_key}
    {"token", "VARCHAR(6) NOT NULL"}
    {"title", text}
    {"file_name", text}
    {"thumb_name", text}
    {"huge_name", text}
    {"metadata", "hstore"}
    {"views", integer}
    {"created_at", time}
    {"updated_at", time}

    "PRIMARY KEY (id)"
  }

  create_index "images", "user_id"
  create_index "images", "album_id"
  create_index "images", "token", unique: true

  -- Access
  create_table "accesstokens", {
    {"id", serial}
    {"user_id", foreign_key}
    {"album_id", foreign_key}
    {"slug", text}
    {"views", integer}
    {"created_at", time}
    {"updated_at", time}
    {"expires_at", time}

    "PRIMARY KEY (id)"
  }
  create_index "accesstokens", "user_id"
  create_index "accesstokens", "album_id"

--migrations.create_migrations_table!

destroy_schema = ->
    tbls = {
      "users", "albums", "images", "accesstokens"
    }

    for t in *tbls
        drop_table t

{ :make_schema, :destroy_schema }

db = require "lapis.nginx.postgres"
schema = require "lapis.db.schema"
migrations = require "lapis.db.migrations"

import types, create_table, create_index, drop_table from schema

make_schema = ->
  {
    :serial
    :varchar
    :text
    :time
    :integer
    :foreign_key
    :boolean
  } = schema.types

  -- Users
  create_table "users", {
    {"id", serial}
    {"token", "VARCHAR(6) NOT NULL"}
    {"email", text}
    {"name", text}
    {"created_at", time}
    {"updated_at", time}

    "PRIMARY KEY (id)"
  }

  create_index "users", "email", unique: true
  create_index "users", "token", unique: true

  -- Albums
  create_table "albums", {
    {"id", serial}
    {"user_id", foreign_key}
    {"token", "VARCHAR(6) NOT NULL"}
    {"title", text}
    {"metadata", "hstore"}
    {"views", integer}
    {"created_at", time}
    {"updated_at", time}

    "PRIMARY KEY (id)"
  }

  create_index "albums", "user_id"
  create_index "albums", "token", unique: true

  -- Images
  create_table "images", {
    {"id", serial}
    {"user_id", foreign_key}
    {"album_id", foreign_key}
    {"token", "VARCHAR(6) NOT NULL"}
    {"title", text}
    {"file_name", text}
    {"thumb_name", text}
    {"huge_name", text}
    {"metadata", "hstore"}
    {"views", integer}
    {"created_at", time}
    {"updated_at", time}

    "PRIMARY KEY (id)"
  }

  create_index "images", "user_id"
  create_index "images", "album_id"
  create_index "images", "token", unique: true

  -- Access
  create_table "accesstokens", {
    {"id", serial}
    {"user_id", foreign_key}
    {"album_id", foreign_key}
    {"slug", text}
    {"views", integer}
    {"created_at", time}
    {"updated_at", time}
    {"expires_at", time}

    "PRIMARY KEY (id)"
  }
  create_index "accesstokens", "user_id"
  create_index "accesstokens", "album_id"
  create_index "accesstokens", "token", unique: true

  -- Persona session
  create_table "sessions", {
    {"sessionid", "VARCHAR(32)"}
    {"email", foreign_key}
    {"created_at", time}
    {"updated_at", time}
    {"expires_at", time}

    "PRIMARY KEY (sessionid)"
  }
  create_index "sessions", "sessionid", unique: true
  create_index "sessions", "email"

--migrations.create_migrations_table!

destroy_schema = ->
    tbls = {
      "users", "albums", "images", "accesstokens", "sessions"
    }

    for t in *tbls
        drop_table t

{ :make_schema, :destroy_schema }


cancel test form as table

cancel test form as table

model = require('model')
quizs = require('quiz.all')

bgcolor = (task, i) ->
  form = task\quiz().state == model.ACTIVE
  bc = 'white'
  if not form and task\ans_i(i) == 1
    bc = 'green'
  if task\ans_i(i) == task.selected
    bc = 'yellow'
  return bc

class Helpers
  ans_form: (task, i) =>
    url = @url_for("answer", {id: task.id})
    form method: "POST", action: url, ->
      input {type: "hidden", name: "csrf_token",
          value: @new_csrf}
      input type: "hidden", name: "task", value: task.id
      input type: "hidden", name: "ans", value: i
      input type: "submit", value: @_('Select')

  cancel_form: =>
    element 'hr'
    element 'table', -> element 'tr', ->
      element 'td', ->
        url = @url_for("cancel", {id: @quiz.id})
        form method: "POST", action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input {type: "submit", value: @_('Cancel quiz'),
            style: 'background-color: pink'}
      element 'td', ->
        font color: 'red', @_("Dangerous zone")

  help_for: (task) =>
    quiz_name = task\quiz().name
    func = quizs[quiz_name][task.name]
    r, a1, a2, a3, a4, help = func(@)
    return help

  task_table: (task) =>
    form = task\quiz().state == model.ACTIVE
    element "table", border: 1, ->
      element "tr", ->
        element "td", colspan: 4, style: "padding:10px", ->
          if task\quiz().state == model.FINISHED
            b task.name
          p @help_for(task)
          pre task.text
      element "tr", ->
        for i = 1, 4
          element "td", {width: 100, style: "padding:10px",
              bgcolor: bgcolor(task, i)}, ->
            pre task\ans(i)
            if form
              @ans_form task, i

  tasks_number_switcher: (quiz) =>
    element 'table', -> element 'tr', ->
      element 'td', ->
        url = @url_for('all-tasks', {id: quiz.id})
        form method: 'POST', action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input {type: 'submit',
            value: @_[[All tasks at one page]]}
      element 'td', ->
        url = @url_for('one-task', {id: quiz.id})
        form method: 'POST', action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input {type: 'submit',
            value: @_[[One task at one page]]}

  new_test_button: (name) =>
    url = @url_for("new-quiz")
    form method: "POST", action: url, ->
      input {type: "hidden", name: "csrf_token",
          value: @new_csrf}
      input {type: "hidden", name: "name", value: name}
      input type: "submit", value: name

  finish_quiz_button: =>
    element 'table', -> element 'tr', ->
      element 'td', ->
        url = @url_for("finish", {id: @quiz.id})
        form method: "POST", action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input type: "submit", value: @_('Finish this quiz')
      element 'td', ->
        font color: 'red', @_([[Quiz is not finished untill
          you press the button]])

model = require('model')
quizs = require('quiz.all')

bgcolor = (task, i) ->
  form = task\quiz().state == model.ACTIVE
  bc = 'white'
  if not form and task\ans_i(i) == 1
    bc = 'green'
  if task\ans_i(i) == task.selected
    bc = 'yellow'
  return bc

class Helpers
  ans_form: (task, i) =>
    url = @url_for("answer", {id: task.id})
    form method: "POST", action: url, ->
      input {type: "hidden", name: "csrf_token",
          value: @new_csrf}
      input type: "hidden", name: "task", value: task.id
      input type: "hidden", name: "ans", value: i
      input type: "submit", value: @_('Select')

  cancel_form: =>
    url = @url_for("cancel", {id: @quiz.id})
    raw '

'
    element 'hr'
    raw '

'
    font color: 'red', @_("Dangerous zone")
    form method: "POST", action: url, ->
      input {type: "hidden", name: "csrf_token",
          value: @new_csrf}
      input {type: "submit", value: @_('Cancel quiz'),
        style: 'background-color: pink'}

  help_for: (task) =>
    quiz_name = task\quiz().name
    func = quizs[quiz_name][task.name]
    r, a1, a2, a3, a4, help = func(@)
    return help

  task_table: (task) =>
    form = task\quiz().state == model.ACTIVE
    element "table", border: 1, ->
      element "tr", ->
        element "td", colspan: 4, style: "padding:10px", ->
          if task\quiz().state == model.FINISHED
            b task.name
          p @help_for(task)
          pre task.text
      element "tr", ->
        for i = 1, 4
          element "td", {width: 100, style: "padding:10px",
              bgcolor: bgcolor(task, i)}, ->
            pre task\ans(i)
            if form
              @ans_form task, i

  tasks_number_switcher: (quiz) =>
    element 'table', -> element 'tr', ->
      element 'td', ->
        url = @url_for('all-tasks', {id: quiz.id})
        form method: 'POST', action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input {type: 'submit',
            value: @_[[All tasks at one page]]}
      element 'td', ->
        url = @url_for('one-task', {id: quiz.id})
        form method: 'POST', action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input {type: 'submit',
            value: @_[[One task at one page]]}

  new_test_button: (name) =>
    url = @url_for("new-quiz")
    form method: "POST", action: url, ->
      input {type: "hidden", name: "csrf_token",
          value: @new_csrf}
      input {type: "hidden", name: "name", value: name}
      input type: "submit", value: name

  finish_quiz_button: =>
    element 'table', -> element 'tr', ->
      element 'td', ->
        url = @url_for("finish", {id: @quiz.id})
        form method: "POST", action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input type: "submit", value: @_('Finish this quiz')
      element 'td', ->
        font color: 'red', @_([[Quiz is not finished untill
          you press the button]])


allow schema creation without user for easy deploy

lapis = require "lapis"
io = require "io"
import capture_errors from require "lapis.application"
import json_params, respond_to, capture_errors, capture_errors_json, assert_error, yield_error from require "lapis.application"
import validate, assert_valid from require "lapis.validate"
import escape_pattern, trim_filter, from_json, to_json from require "lapis.util"
db = require "lapis.db"
import Redis, Users, Albums, Images, Accesstokens, generate_token, imagedatesql from require "photongx.models"
config = require("lapis.config").get!

require_login = (fn) ->
  =>
    if @current_user
      fn @
    else
      redirect_to: @url_for "admin"

persona_verify = (assertion, audience) ->

    options = { method:ngx.HTTP_POST, body:to_json {:assertion, :audience} }
    res, err = ngx.location.capture('/persona/', options)

    if not res then
      return { err: res }

    if res.status >= 200 and res.status < 300 then
      return from_json res.body
    else
      return {
        status:res.status,
        body:res.body
      }

class extends lapis.Application
  layout: require "photongx.views.layout"
  views_prefix: "photongx.views"

  @before_filter =>
    @current_user = Users\read_session @

  [index: "/"]: =>
    if @current_user
      return redirect_to: @url_for "albums"
    render:true

  [albums: "/albums"]: require_login =>
    @albums = Albums\select "where user_id = ? order by id DESC", @current_user.id
    --@albums = Albums\select "left join images on albums.id=images.album_id where albums.user_id = ? order by albums.id DESC, images.views DESC", @current_user.id, fields:"distinct on (albums.id) *"
    --images = Images\get_coverimages @albums
    for i=1, #@albums
      album = @albums[i]
      album.image = (Images\get_coverimage album.id)[1]
      album.url = @url_for("album", token:album.token, title:album.title)
    render: true

  [tokenalbums: "/albums/:slug/"]: capture_errors =>
    @accesstokens = Accesstokens\for_slug @params.slug
    unless #@accesstokens > 0
      return render: "error", status: 404
    album_ids = [a.album_id for a in *@accesstokens]
    @albums = Albums\find_all album_ids

    -- FIXME improve SQL to single statement
    for album in *@albums
      images = Images\get_coverimage album.id
      album.image = images[1]
      -- Override the token with our given slug so the template generates the correct URLs
      album.slug = @params.slug
      album.url = @url_for("tokenalbum", slug:@params.slug, token:album.token, title:album.title)
    render: "albums"

  [album: "/album/:token/:title/"]: =>
    if @current_user
      @album = Albums\find token:@params.token
      unless @album
        return render:"error", status:404
      unless @current_user.id == @album.user_id
        return render:"error", status:403
    unless @album return render:"error", status:403
    @album.views = @album.views + 1
    @album\update "views"
    @images = Images\select "where album_id = ? ORDER BY date, file_name", @album.id, fields: "*, "..imagedatesql
    @albumurl = @url_for('album', token:@params.token, title:@album.title)
    @albumsurl = @url_for('albums')
    render: true

  [tokenalbum: "/album/:slug/:token/:title/"]: =>
    @album = Albums\find token:@params.token
    unless @album
      return render:"error", status:404
    valid_token = Accesstokens\validate_album @params.slug, @album.id
    unless valid_token
      return render:"error", status:410
    @albumurl = @url_for('tokenalbum', slug:@params.slug, token:@album.token, title:@album.title)
    @albumsurl = @url_for('tokenalbums', slug:@params.slug)
    @album.views = @album.views + 1
    @album\update "views"
    @images = Images\select "where album_id = ? ORDER BY date, file_name", @album.id, fields: "*, "..imagedatesql
    render: "album"

--
-- This view gets called from a rewrite_by_lua handler so it exits
-- because: "Note that when calling ngx.exit(ngx.OK) within a rewrite_by_lua handler, the nginx request processing control flow will still continue to the content handler."
--
  [img: "/img/:token/:filename"]: =>

    -- Check if user has permission to see image or return 403
    -- Check if token is valid
    -- Set real url or return 404

    -- Simple auth check for now
    --unless @current_user
    --  ngx.exit(403)

    -- TODO maybe disallow original?

    @image = Images\find token:@params.token
    unless @image
      ngx.exit(404)

    -- Check access tokens and return status 410 if expired
    imguri = '/real' .. @image\file_path! .. '/' ..@params.filename
    ngx.req.set_uri imguri, true

-- FIXME require_login
  [zipalbum: "/archive/album/:token/:title/"]: =>
    if @current_user
      @album = Albums\find token:@params.token
      unless @album
        return render:"error", status:404
      unless @current_user.id == @album.user_id
        return render:"error", status:403
    -- FIXME
    @album = Albums\find token:@params.token
    unless @album return render:"error", status:403

    @images = Images\select "where album_id = ? ORDER BY date, file_name", @album.id, fields: "*, "..imagedatesql

    -- Craft the body request that modzip wants

    reqbody = {}

    sample = "- 428    /foo.txt   My Document1.txt"

    for image in *@images
      size = image\get_file_size!
      url = image\real_file_name!
      name = image.file_name
      table.insert(reqbody, table.concat({'-', size, url, name}, ' '))

    reqbody = table.concat(reqbody, '\n')

    --do
    --  json:{:reqbody}

    ngx.header["X-Archive-Files"] = 'zip'
    ngx.header['Content-Type'] = 'application/force-download'
    ngx.header['Content-Disposition'] = "attachment; filename=#{@album.title}.zip"
    ngx.req.set_header "X-Archive-Files", 'zip'
    -- Capture the zip file from modzip
    options = { method:ngx.HTTP_GET, body:reqbody }
    res, err = ngx.location.capture('/zip', options)

    unless res
      return render:"error", status:500

    --if res.status >= 200 and res.status < 300 then
    --return layout:false, res.body
    return layout:false, reqbody

  [persona_login: "/api/persona/login"]: respond_to {
    POST: capture_errors_json =>
      body = ngx.req.get_body_data!
      if body
        body = from_json body

        verification_data = persona_verify body.assertion, config.site
        if verification_data.status == 'okay'
          Users\write_session @, verification_data
          return json:verification_data
      json: {email:false}
  }
  [persona_status: "/api/persona/status"]: respond_to {
    GET: =>
      render: true

    POST: capture_errors_json =>
      cu = @current_user
      if cu
        return json:{email:cu.email}
      json:{email:false}
  }
  [user_logout: "/api/persona/logout"]: =>
    @session.user = false
    json: {email:false}

-- ALBUMS API view - get albums or add new album
  [apialbums: "/api/albums"]: respond_to {
    GET: capture_errors_json require_login =>
      albums = assert_error Albums\select "where user_id = ?", @current_user.id
      json: {:albums}

    POST: capture_errors_json require_login json_params =>

      assert_valid @params, {
        { "name", exists: true, min_length: 1 }
      }
      -- Get or create
      album = Albums\find user_id: @current_user.id, title:@params.name
      unless album
        album = Albums\create @current_user.id, @params.name
      json: { album: album }
  }

  "/api/albums/:album_id": respond_to {
    PUT: capture_errors_json require_login json_params =>
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      album.title = @params.title
      album\update "title"
      json: {:album}
    DELETE: capture_errors_json require_login =>
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      unless album
        return render:"error", status:404
      album\delete!
      json:album
  }

  "/api/images/:album_id": respond_to {

    GET: capture_errors_json require_login =>
      album = Albums\find id:@params.album_id
      unless album
        return render:"error", status:404
      unless album.user_id == @current_user.id
        return render:"error", status:403
      images = assert_error Images\select "where album_id = ? ORDER BY date", @params.album_id, fields: "*, "..imagedatesql
      --- TODO maybe use raw query to cast hstore to json?
      -- Examples found:https://gist.github.com/WoLpH/2318757
      -- Or use postgresql 9.3 which can cast hstore to JSON
      for image in *images
        if image.metadata
          newstr, n, err = ngx.re.gsub(image.metadata, "=>", ":")
          image.metadata = from_json '{'..newstr..'}'
      json: {:images}
  }

  "/api/accesstokens/:album_id": respond_to {

    GET: capture_errors_json require_login =>
      accesstokens = Accesstokens\select "where user_id = ? and album_id = ?", @current_user.id, @params.album_id
      json: {:accesstokens}

    DELETE: =>
      accesstoken = Accesstokens\find id:@params.album_id, user_id:@current_user.id
      unless accesstoken
        return render:"error", status:404
      accesstoken\delete!
      json:{ :accesstoken }
  }

  [images: "/api/images"]: respond_to {
    GET:capture_errors_json require_login  =>
      images = assert_error Images\select "where user_id = ?", @current_user.id
      json: {:images}

    POST: capture_errors_json require_login =>
        assert_valid @params, {
            {'upload', file_exists: true}
            {'filename', exists: true}
            {'title', exists: true}
            {'token', exists: true}
            {'checksum', exists: true}
        }
        {:upload, :title, :filename, :token, :checksum} = @params
        pattern = '\\.(jpe?g|gif|png|crw|raw)$'
        unless ngx.re.match(filename, pattern, "i")
            return status:403, json:{status:403, result:'Filename must be of image type'}
        file = @params.upload
        album = assert_error Albums\find user_id: @current_user.id, token:token
        success, image = pcall -> Images\create @current_user.id, album.id, filename
        -- Since there is a likely chance our silly token can be duplicate we just try to generate image again if it fails
        while not success
          success, image = pcall -> Images\create @current_user.id, album.id, filename
        content = file.content
        real_file_name = image\real_file_name!
        diskfile = io.open real_file_name, 'w'
        unless diskfile
          -- TODO delete created image from SQL
          return status:403, json:{status:403, error:"Permission denied"}
        diskfile\write file.content
        diskfile\flush!
        diskfile\close
        redis = Redis!
        queue = assert_error redis\queue image.token
        status:200, json:{status:200, result:'Image successfully uploaded'}
  }

  [photostreamimages: "/api/photostreamimages"]: respond_to {
    GET:capture_errors_json require_login  =>
      photostreamimages = assert_error Images\select "WHERE user_id = ? ORDER BY date DESC", @current_user.id, fields: "*, "..imagedatesql
      json: {:photostreamimages}
    }

  "/api/albumttl/:album_id": respond_to {
    POST: capture_errors_json require_login =>
      assert_valid @params, {
        { "name", exists: true}
        { "ttl", exists: true}
      }
      ttl = tonumber @params.ttl
      -- TTL not a number ? Assume forever.
      if ttl == nil
          ttl = 2^32 -- ~150 years

      name = @params.name
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      accesstoken = Accesstokens\create @current_user.id, album.id, name, ttl

      json: {:album,:accesstoken}
    }

  [admin: "/admin/"]: =>
    layout:'admin'

  "/api/tag": =>
    json: {token: generate_token 6}

  "/api/image/:image_id": respond_to {
    DELETE: capture_errors_json require_login =>
      image = Images\find id:@params.image_id, user_id: @current_user.id
      unless image
        return render:"error", status:"404"
      image\delete!
      json:image
  }

  "/api/img/click": =>
    assert_valid @params, {
      { "img", exists: true, min_length: 6 }
    }
    @image = Images\find token:@params.img
    unless @image
      return render: "error", status: 404
    @image.views = @image.views + 1
    @image\update "views"

    json: {views: @image.views}

  [adminapi: "/admin/api/all"]: =>
    ok = "test"
    json: { status: ok}

  "/api/queue": require_login capture_errors_json =>
    redis = Redis!
    queue = assert_error redis\queue_length!
    json: {counter:queue}

  "/db/make": =>
    schema = require "schema"
    schema.make_schema!
    json: { status: "ok" }

  "/db/destroy": require_login =>
    -- Hard coded to first user for now
    if @current_user.id == 1
      schema = require "schema"
      schema.destroy_schema!
      return json: { status: "ok" }
    json: status: 403

  "/db/migrate": require_login =>
    -- Hard coded to first user for now
    if @current_user.id == 1
      import run_migrations from require "lapis.db.migrations"
      run_migrations require "migrations"
      return json: { status: "ok" }
    json: status: 403

  "/debug": =>
    layout:false, "hello"

lapis = require "lapis"
import capture_errors from require "lapis.application"
import json_params, respond_to, capture_errors, capture_errors_json, assert_error, yield_error from require "lapis.application"
import validate, assert_valid from require "lapis.validate"
import escape_pattern, trim_filter, from_json, to_json from require "lapis.util"
db = require "lapis.db"
import Redis, Users, Albums, Images, Accesstokens, generate_token, imagedatesql from require "photongx.models"
config = require("lapis.config").get!

require_login = (fn) ->
  =>
    if @current_user
      fn @
    else
      redirect_to: @url_for "admin"

persona_verify = (assertion, audience) ->

    options = { method:ngx.HTTP_POST, body:to_json {:assertion, :audience} }
    res, err = ngx.location.capture('/persona/', options)

    if not res then
      return { err: res }

    if res.status >= 200 and res.status < 300 then
      return from_json res.body
    else
      return {
        status:res.status,
        body:res.body
      }

class extends lapis.Application
  layout: require "photongx.views.layout"
  views_prefix: "photongx.views"

  @before_filter =>
    @current_user = Users\read_session @

  [index: "/"]: =>
    if @current_user
      return redirect_to: @url_for "albums"
    render:true

  [albums: "/albums"]: require_login =>
    @albums = Albums\select "where user_id = ? order by id DESC", @current_user.id
    --@albums = Albums\select "left join images on albums.id=images.album_id where albums.user_id = ? order by albums.id DESC, images.views DESC", @current_user.id, fields:"distinct on (albums.id) *"
    --images = Images\get_coverimages @albums
    for i=1, #@albums
      album = @albums[i]
      album.image = (Images\get_coverimage album.id)[1]
      album.url = @url_for("album", token:album.token, title:album.title)
    render: true

  [tokenalbums: "/albums/:slug/"]: capture_errors =>
    @accesstokens = Accesstokens\for_slug @params.slug
    unless #@accesstokens > 0
      return render: "error", status: 404
    album_ids = [a.album_id for a in *@accesstokens]
    @albums = Albums\find_all album_ids

    -- FIXME improve SQL to single statement
    for album in *@albums
      images = Images\get_coverimage album.id
      album.image = images[1]
      -- Override the token with our given slug so the template generates the correct URLs
      album.slug = @params.slug
      album.url = @url_for("tokenalbum", slug:@params.slug, token:album.token, title:album.title)
    render: "albums"

  [album: "/album/:token/:title/"]: =>
    if @current_user
      @album = Albums\find token:@params.token
      unless @album
        return render:"error", status:404
      unless @current_user.id == @album.user_id
        return render:"error", status:403
    unless @album return render:"error", status:403
    @album.views = @album.views + 1
    @album\update "views"
    @images = Images\select "where album_id = ? ORDER BY date, file_name", @album.id, fields: "*, "..imagedatesql
    @albumurl = @url_for('album', token:@params.token, title:@album.title)
    @albumsurl = @url_for('albums')
    render: true

  [tokenalbum: "/album/:slug/:token/:title/"]: =>
    @album = Albums\find token:@params.token
    unless @album
      return render:"error", status:404
    valid_token = Accesstokens\validate_album @params.slug, @album.id
    unless valid_token
      return render:"error", status:410
    @albumurl = @url_for('tokenalbum', slug:@params.slug, token:@album.token, title:@album.title)
    @albumsurl = @url_for('tokenalbums', slug:@params.slug)
    @album.views = @album.views + 1
    @album\update "views"
    @images = Images\select "where album_id = ? ORDER BY date, file_name", @album.id, fields: "*, "..imagedatesql
    render: "album"

--
-- This view gets called from a rewrite_by_lua handler so it exits
-- because: "Note that when calling ngx.exit(ngx.OK) within a rewrite_by_lua handler, the nginx request processing control flow will still continue to the content handler."
--
  [img: "/img/:token/:filename"]: =>

    -- Check if user has permission to see image or return 403
    -- Check if token is valid
    -- Set real url or return 404

    -- Simple auth check for now
    --unless @current_user
    --  ngx.exit(403)

    -- TODO maybe disallow original?

    @image = Images\find token:@params.token
    unless @image
      ngx.exit(404)

    -- Check access tokens and return status 410 if expired
    imguri = '/real' .. @image\file_path! .. '/' ..@params.filename
    ngx.req.set_uri imguri, true

  [persona_login: "/api/persona/login"]: respond_to {
    POST: capture_errors_json =>
      body = ngx.req.get_body_data!
      if body
        body = from_json body

        verification_data = persona_verify body.assertion, config.site
        if verification_data.status == 'okay'
          Users\write_session @, verification_data
          return json:verification_data
      json: {email:false}
  }
  [persona_status: "/api/persona/status"]: respond_to {
    GET: =>
      render: true

    POST: capture_errors_json =>
      cu = @current_user
      if cu
        return json:{email:cu.email}
      json:{email:false}
  }
  [user_logout: "/api/persona/logout"]: =>
    @session.user = false
    json: {email:false}

-- ALBUMS API view - get albums or add new album
  [apialbums: "/api/albums"]: respond_to {
    GET: capture_errors_json require_login =>
      albums = assert_error Albums\select "where user_id = ?", @current_user.id
      json: {:albums}

    POST: capture_errors_json require_login json_params =>

      assert_valid @params, {
        { "name", exists: true, min_length: 1 }
      }
      -- Get or create
      album = Albums\find user_id: @current_user.id, title:@params.name
      unless album
        album = Albums\create @current_user.id, @params.name
      json: { album: album }
  }

  "/api/albums/:album_id": respond_to {
    PUT: capture_errors_json require_login json_params =>
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      album.title = @params.title
      album\update "title"
      json: {:album}
    DELETE: capture_errors_json require_login =>
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      unless album
        return render:"error", status:404
      album\delete!
      json:album
  }

  "/api/images/:album_id": respond_to {

    GET: capture_errors_json require_login =>
      album = Albums\find id:@params.album_id
      unless album
        return render:"error", status:404
      unless album.user_id == @current_user.id
        return render:"error", status:403
      images = assert_error Images\select "where album_id = ? ORDER BY date", @params.album_id, fields: "*, "..imagedatesql
      --- TODO maybe use raw query to cast hstore to json?
      -- Examples found:https://gist.github.com/WoLpH/2318757
      -- Or use postgresql 9.3 which can cast hstore to JSON
      for image in *images
        if image.metadata
          newstr, n, err = ngx.re.gsub(image.metadata, "=>", ":")
          image.metadata = from_json '{'..newstr..'}'
      json: {:images}
  }

  "/api/accesstokens/:album_id": respond_to {

    GET: capture_errors_json require_login =>
      accesstokens = Accesstokens\select "where user_id = ? and album_id = ?", @current_user.id, @params.album_id
      json: {:accesstokens}

    DELETE: =>
      accesstoken = Accesstokens\find id:@params.album_id, user_id:@current_user.id
      unless accesstoken
        return render:"error", status:404
      accesstoken\delete!
      json:{ :accesstoken }
  }

  [images: "/api/images"]: respond_to {
    GET:capture_errors_json require_login  =>
      images = assert_error Images\select "where user_id = ?", @current_user.id
      json: {:images}

    POST: capture_errors_json require_login =>
        assert_valid @params, {
            {'upload', file_exists: true}
            {'filename', exists: true}
            {'title', exists: true}
            {'token', exists: true}
            {'checksum', exists: true}
        }
        {:upload, :title, :filename, :token, :checksum} = @params
        pattern = '\\.(jpe?g|gif|png|crw|raw)$'
        unless ngx.re.match(filename, pattern, "i")
            return status:403, json:{status:403, result:'Filename must be of image type'}
        file = @params.upload
        album = assert_error Albums\find user_id: @current_user.id, token:token
        success, image = pcall -> Images\create @current_user.id, album.id, filename
        -- Since there is a likely chance our silly token can be duplicate we just try to generate image again if it fails
        while not success
          success, image = pcall -> Images\create @current_user.id, album.id, filename
        content = file.content
        real_file_name = image\real_file_name!
        diskfile = io.open real_file_name, 'w'
        unless diskfile
          -- TODO delete created image from SQL
          return status:403, json:{status:403, error:"Permission denied"}
        diskfile\write file.content
        diskfile\flush!
        diskfile\close
        redis = Redis!
        queue = assert_error redis\queue image.token
        status:200, json:{status:200, result:'Image successfully uploaded'}
  }

  [photostreamimages: "/api/photostreamimages"]: respond_to {
    GET:capture_errors_json require_login  =>
      photostreamimages = assert_error Images\select "WHERE user_id = ? ORDER BY date DESC", @current_user.id, fields: "*, "..imagedatesql
      json: {:photostreamimages}
    }

  "/api/albumttl/:album_id": respond_to {
    POST: capture_errors_json require_login =>
      assert_valid @params, {
        { "name", exists: true}
        { "ttl", exists: true}
      }
      ttl = tonumber @params.ttl
      -- TTL not a number ? Assume forever.
      if ttl == nil
          ttl = 2^32 -- ~150 years

      name = @params.name
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      accesstoken = Accesstokens\create @current_user.id, album.id, name, ttl

      json: {:album,:accesstoken}
    }

  [admin: "/admin/"]: =>
    layout:'admin'

  "/api/tag": =>
    json: {token: generate_token 6}

  "/api/image/:image_id": respond_to {
    DELETE: capture_errors_json require_login =>
      image = Images\find id:@params.image_id, user_id: @current_user.id
      unless image
        return render:"error", status:"404"
      image\delete!
      json:image
  }

  "/api/img/click": =>
    assert_valid @params, {
      { "img", exists: true, min_length: 6 }
    }
    @image = Images\find token:@params.img
    unless @image
      return render: "error", status: 404
    @image.views = @image.views + 1
    @image\update "views"

    json: {views: @image.views}

  [adminapi: "/admin/api/all"]: =>
    ok = "test"
    json: { status: ok}

  "/api/queue": require_login capture_errors_json =>
    redis = Redis!
    queue = assert_error redis\queue_length!
    json: {counter:queue}

  "/db/make": require_login =>
    -- Hard coded to first user for now
    if @current_user.id == 1
      schema = require "schema"
      schema.make_schema!
      return json: { status: "ok" }
    json: status: 403

  "/db/destroy": require_login =>
    -- Hard coded to first user for now
    if @current_user.id == 1
      schema = require "schema"
      schema.destroy_schema!
      return json: { status: "ok" }
    json: status: 403

  "/db/migrate": require_login =>
    -- Hard coded to first user for now
    if @current_user.id == 1
      import run_migrations from require "lapis.db.migrations"
      run_migrations require "migrations"
      return json: { status: "ok" }
    json: status: 403

  "/debug": =>
    session_cache = ngx.shared.session_cache
    user = from_json session_cache\get @session.user.email
    json:{session:@session.user, :user, current_user:@current_user}


Forgot to return a value in 'extract_compressed_2d'.

-- 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"
Retrace = require "glurg.common.Retrace"
TextureBlob = require "glurg.resources.TextureBlob"
Call = require "glurg.trace.Call"

marshal_texture_enumeration = (enumeration, value) ->
	Promise.keep("enumeration", Promise.IsString(enumeration))
	Promise.keep("value", Promise.IsString(value))

	v = glurg.resources.texture["#{enumeration}_#{value}"]
	if not v then
		error "enumeration not found"

	return v

marshal_description = (description) ->
	Promise.keep("description", Promise.IsTable(description))
	Promise.keep("description.swizzle", Promise.IsString(description.swizzle))
	Promise.keep("description.storage", Promise.IsString(description.storage))
	Promise.keep("description.bit_size", Promise.IsNumber(description.bit_size))

	result = glurg.resources.PixelComponentDescription.new!
	result.swizzle = marshal_texture_enumeration('swizzle', description.swizzle)
	result.storage = marshal_texture_enumeration('storage', description.storage)
	result.bit_size = description.bit_size

	return result

class TextureBlobBuilder
	new: =>
		@_builder = glurg.resources.TextureBlobBuilder.new!

		@\set_red_description({ swizzle: 'red', storage: 'disabled', bit_size: 0 })
		@\set_green_description({ swizzle: 'green', storage: 'disabled', bit_size: 0 })
		@\set_blue_description({ swizzle: 'blue', storage: 'disabled', bit_size: 0 })
		@\set_alpha_description({ swizzle: 'alpha', storage: 'disabled', bit_size: 0 })
		@\set_wrap_mode({ s: 'repeat', t: 'repeat', r: 'repeat' })
		@\set_type('2d')
		@\set_minification_filter('nearest')
		@\set_magnification_filter('nearest')
		@\set_width(1)
		@\set_height(1)
		@\set_depth(1)
		@\set_mipmap_level(0)

		@has_pixel_data = false

	set_red_description: (description) =>
		@_builder\set_red_description(marshal_description(description))

	set_green_description: (description) =>
		@_builder\set_green_description(marshal_description(description))

	set_blue_description: (description) =>
		@_builder\set_blue_description(marshal_description(description))

	set_alpha_description: (description) =>
		@_builder\set_alpha_description(marshal_description(description))

	set_wrap_mode: (wrap_mode) =>
		Promise.keep("wrap_mode", Promise.IsTable(wrap_mode))

		@_builder\set_wrap_mode(0,
			marshal_texture_enumeration('wrap', wrap_mode.s or 'repeat'))
		@_builder\set_wrap_mode(1,
			marshal_texture_enumeration('wrap', wrap_mode.t or 'repeat'))
		@_builder\set_wrap_mode(2,
			marshal_texture_enumeration('wrap', wrap_mode.r or 'repeat'))

	set_type: (type) =>
		@_builder\set_type(marshal_texture_enumeration('type', type))

	set_minification_filter: (filter) =>
		@_builder\set_minification_filter(marshal_texture_enumeration('filter', filter))

	set_magnification_filter: (filter) =>
		@_builder\set_magnification_filter(marshal_texture_enumeration('filter', filter))

	set_width: (width) =>
		Promise.keep("width", width >= 1)

		@_builder\set_width(width)

	set_height: (height) =>
		Promise.keep("height", height >= 1)

		@_builder\set_height(height)

	set_depth: (depth) =>
		@_builder\set_depth(depth)

	set_mipmap_level: (mipmap_level) =>
		@_builder\set_mipmap_level(mipmap_level)

	build: =>
		Promise.keep("@has_pixel_data", @has_pixel_data == true)

		return TextureBlob(@_builder\build!)

	extract_2d: (call) =>
		Promise.keep("call", Promise.IsClass(call, Call))

		@\set_width(call\get_argument_by_name("width")\query!)
		@\set_height(call\get_argument_by_name("height")\query!)
		@\set_depth(1)
		@\set_mipmap_level(call\get_argument_by_name("level"))

		pixel_data = call\get_argument_by_name("pixels")\query!
		if pixel_data == nil or pixel_data.length == 0
			return false

		pixel_format = call\get_argument_by_name("format")\query!.value\query!
		pixel_type = call\get_argument_by_name("type")\query!.value\query!

		@has_pixel_data = @_builder\extract_from_call(pixel_format,
			pixel_type,
			pixel_data.data)

		return @has_pixel_data

	extract_compressed_2d: (call) =>
		Promise.keep("call", Promise.IsClass(call, Call))

		@\set_width(call\get_argument_by_name("width")\query!)
		@\set_height(call\get_argument_by_name("height")\query!)
		@\set_depth(1)
		@\set_mipmap_level(call\get_argument_by_name("data"))

		pixel_data = call\get_argument_by_name("data")\query!
		if pixel_data == nil or pixel_data.length == 0
			return false

		format = call\get_argument_by_name("format")\query!
		compression = switch format.value_name
			when "GL_COMPRESSED_RGB_S3TC_DXT1_EXT", "GL_COMPRESSED_SRGB_S3TC_DXT1_EXT"
				'bc1'
			when "GL_COMPRESSED_RGB_S3TC_DXT3_EXT", "GL_COMPRESSED_SRGB_S3TC_DXT3_EXT"
				'bc2'
			when "GL_COMPRESSED_RGB_ALPHA_S3TC_DXT5_EXT", "GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT"
				'bc3'
			else
				error "unsupported compression format"

		@has_pixel_data = @_builder\extract_compressed_from_call(
			marshal_texture_enumeration('compression', compression), pixel_data.data)

		return @has_pixel_data

	extract_framebuffer_attachment: (render_state, attachment_type, attachment_index) =>
		Promise.keep("render_state", Promise.IsUserdata(render_state))
		Promise.keep("attachment_type", Promise.IsString(attachment_type))

		@has_pixel_data = switch attachment_type
			when 'depth'
				@_builder\extract_framebuffer_from_state(render_state, -1)
			when 'color'
				Promise.keep("attachment_index", Promise.IsNumber(attachment_index))
				Promise.keep("attachment_index >= 1", attachment_index >= 1)
				@_builder\extract_framebuffer_from_state(render_state, attachment_index - 1)
			else
				error "invalid attachment type"

		return @has_pixel_data

return TextureBlobBuilder

-- 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"
Retrace = require "glurg.common.Retrace"
TextureBlob = require "glurg.resources.TextureBlob"
Call = require "glurg.trace.Call"

marshal_texture_enumeration = (enumeration, value) ->
	Promise.keep("enumeration", Promise.IsString(enumeration))
	Promise.keep("value", Promise.IsString(value))

	v = glurg.resources.texture["#{enumeration}_#{value}"]
	if not v then
		error "enumeration not found"

	return v

marshal_description = (description) ->
	Promise.keep("description", Promise.IsTable(description))
	Promise.keep("description.swizzle", Promise.IsString(description.swizzle))
	Promise.keep("description.storage", Promise.IsString(description.storage))
	Promise.keep("description.bit_size", Promise.IsNumber(description.bit_size))

	result = glurg.resources.PixelComponentDescription.new!
	result.swizzle = marshal_texture_enumeration('swizzle', description.swizzle)
	result.storage = marshal_texture_enumeration('storage', description.storage)
	result.bit_size = description.bit_size

	return result

class TextureBlobBuilder
	new: =>
		@_builder = glurg.resources.TextureBlobBuilder.new!

		@\set_red_description({ swizzle: 'red', storage: 'disabled', bit_size: 0 })
		@\set_green_description({ swizzle: 'green', storage: 'disabled', bit_size: 0 })
		@\set_blue_description({ swizzle: 'blue', storage: 'disabled', bit_size: 0 })
		@\set_alpha_description({ swizzle: 'alpha', storage: 'disabled', bit_size: 0 })
		@\set_wrap_mode({ s: 'repeat', t: 'repeat', r: 'repeat' })
		@\set_type('2d')
		@\set_minification_filter('nearest')
		@\set_magnification_filter('nearest')
		@\set_width(1)
		@\set_height(1)
		@\set_depth(1)
		@\set_mipmap_level(0)

		@has_pixel_data = false

	set_red_description: (description) =>
		@_builder\set_red_description(marshal_description(description))

	set_green_description: (description) =>
		@_builder\set_green_description(marshal_description(description))

	set_blue_description: (description) =>
		@_builder\set_blue_description(marshal_description(description))

	set_alpha_description: (description) =>
		@_builder\set_alpha_description(marshal_description(description))

	set_wrap_mode: (wrap_mode) =>
		Promise.keep("wrap_mode", Promise.IsTable(wrap_mode))

		@_builder\set_wrap_mode(0,
			marshal_texture_enumeration('wrap', wrap_mode.s or 'repeat'))
		@_builder\set_wrap_mode(1,
			marshal_texture_enumeration('wrap', wrap_mode.t or 'repeat'))
		@_builder\set_wrap_mode(2,
			marshal_texture_enumeration('wrap', wrap_mode.r or 'repeat'))

	set_type: (type) =>
		@_builder\set_type(marshal_texture_enumeration('type', type))

	set_minification_filter: (filter) =>
		@_builder\set_minification_filter(marshal_texture_enumeration('filter', filter))

	set_magnification_filter: (filter) =>
		@_builder\set_magnification_filter(marshal_texture_enumeration('filter', filter))

	set_width: (width) =>
		Promise.keep("width", width >= 1)

		@_builder\set_width(width)

	set_height: (height) =>
		Promise.keep("height", height >= 1)

		@_builder\set_height(height)

	set_depth: (depth) =>
		@_builder\set_depth(depth)

	set_mipmap_level: (mipmap_level) =>
		@_builder\set_mipmap_level(mipmap_level)

	build: =>
		Promise.keep("@has_pixel_data", @has_pixel_data == true)

		return TextureBlob(@_builder\build!)

	extract_2d: (call) =>
		Promise.keep("call", Promise.IsClass(call, Call))

		@\set_width(call\get_argument_by_name("width")\query!)
		@\set_height(call\get_argument_by_name("height")\query!)
		@\set_depth(1)
		@\set_mipmap_level(call\get_argument_by_name("level"))

		pixel_data = call\get_argument_by_name("pixels")\query!
		if pixel_data == nil or pixel_data.length == 0
			return false

		pixel_format = call\get_argument_by_name("format")\query!.value\query!
		pixel_type = call\get_argument_by_name("type")\query!.value\query!

		@has_pixel_data = @_builder\extract_from_call(pixel_format,
			pixel_type,
			pixel_data.data)

		return @has_pixel_data

	extract_compressed_2d: (call) =>
		Promise.keep("call", Promise.IsClass(call, Call))

		@\set_width(call\get_argument_by_name("width")\query!)
		@\set_height(call\get_argument_by_name("height")\query!)
		@\set_depth(1)
		@\set_mipmap_level(call\get_argument_by_name("data"))

		pixel_data = call\get_argument_by_name("data")\query!
		if pixel_data == nil or pixel_data.length == 0
			return false

		format = call\get_argument_by_name("format")\query!
		compression = switch format.value_name
			when "GL_COMPRESSED_RGB_S3TC_DXT1_EXT", "GL_COMPRESSED_SRGB_S3TC_DXT1_EXT"
				'bc1'
			when "GL_COMPRESSED_RGB_S3TC_DXT3_EXT", "GL_COMPRESSED_SRGB_S3TC_DXT3_EXT"
				'bc2'
			when "GL_COMPRESSED_RGB_ALPHA_S3TC_DXT5_EXT", "GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT"
				'bc3'
			else
				error "unsupported compression format"

		@has_pixel_data = @_builder\extract_compressed_from_call(
			marshal_texture_enumeration('compression', compression), pixel_data.data)

	extract_framebuffer_attachment: (render_state, attachment_type, attachment_index) =>
		Promise.keep("render_state", Promise.IsUserdata(render_state))
		Promise.keep("attachment_type", Promise.IsString(attachment_type))

		@has_pixel_data = switch attachment_type
			when 'depth'
				@_builder\extract_framebuffer_from_state(render_state, -1)
			when 'color'
				Promise.keep("attachment_index", Promise.IsNumber(attachment_index))
				Promise.keep("attachment_index >= 1", attachment_index >= 1)
				@_builder\extract_framebuffer_from_state(render_state, attachment_index - 1)
			else
				error "invalid attachment type"

		return @has_pixel_data

return TextureBlobBuilder


github: include diff URL in message

github: include diff URL in message

Former-commit-id: e8d27dbbbea154fbd7de5122f444f75314376a61 [formerly 50df844936861b779108e56a9ad5679246f817ff]
Former-commit-id: c3cfd7c4ef9d48a52fa9e5748cca364c9a8893a5

util = require 'util'

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

  repo = json.repository.full_name
  branch = json.ref\gsub 'refs/heads/', ''

  for c in *json.commits
    ivar2\Msg 'privmsg', destination, nil, "#{repo}: #{branch}, #{c.author.username}: #{c.message} #{c.url}"

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

return {}

util = require 'util'

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

  repo = json.repository.full_name
  branch = json.ref\gsub 'refs/heads/', ''

  for c in *json.commits
    ivar2\Msg 'privmsg', destination, nil, "#{repo}: #{branch} #{c.author.username} #{c.message}"

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

return {}


Added more debug data

Added more debug data

coverage = require "coveralls.coverage"
require "json"

export ^

-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-- This copyright applies to formencodepart and formencode
formencodepart = (s) ->
	return s and s\gsub "%W", (c) ->
			if c ~= " "
				string.format "%%%02x", c\byte!
			else
				"+"

formencode = (form) ->
	result = {}
	if form[1] -- Array of ordered { name, value }
		for _, field in ipairs form  do
			table.insert result, formencodepart(field.name).."="..formencodepart(field.value)
	else -- Unordered map of name -> value
		for name, value in pairs(form) do
			table.insert result, formencodepart(name).."="..formencodepart(value)

	table.concat result, "&"

class coveralls extends coverage.CodeCoverage
	Travis: "travis-ci"
	Jenkins: "jenkins"
	Semaphore: "semaphore"
	Circle: "circleci"
	Local: "local"
	Debug: "debug"

	new: =>
		@source_files = {}
		-- The continuous integration service
		@service_name = @@Travis
		-- The continuous integration service job id
		@service_job_id = nil
		-- The secret repo token for your repository, found at the bottom of
		-- your repository's page on Coveralls.
		@repo_token = nil

		@dirname = 'src'
		@ext = '*.lua'

		super!

	coverDir: (dirname, ext = '*.lua') =>
		dir = require "pl.dir"
		@cover f for f in *(dir.getfiles dirname, ext)
		@coverDir d, ext for d in *(dir.getdirectories dirname)

	cover: (fname) =>
		file_coverage = @file_coverage fname
		return unless file_coverage
		c = file_coverage.coverage
		length = #c
		for k, v in pairs c
			length = k if k > length

		for i = 1, length
			file_coverage.coverage[i] = json.util.null if c[i] == nil

		table.insert @source_files, file_coverage

	send: =>
		if not @service_job_id
			env = nil
			switch @service_name
				when @@Travis
					env = 'TRAVIS_JOB_ID'
				when @@Jenkins
					env = 'BUILD_NUMBER'
				when @@Semaphore
					env = 'SEMAPHORE_BUILD_NUMBER'
				when @@Circle
					env = 'CIRCLE_BUILD_NUM'
				when @@Local
					return
				when @@Debug
					moon = require "moon"
					moon.p @source_files
					json_data = json.encode {
						service_name: @service_name,
						service_job_id: @service_job_id,
						repo_token: @repo_token,
						source_files: @source_files,
					}
					print json_data
					print #json_data
					form_data = formencode { json: json_data }
					print form_data
					print #form_data
					return

			@service_job_id = os.getenv env

		assert @service_job_id, "A service_job_id must be specified"

		form_data = formencode {
			json: json.encode {
				service_name: @service_name,
				service_job_id: @service_job_id,
				repo_token: @repo_token,
				source_files: @source_files,
			}
		}

		https = require "ssl.https"
		body, code, headers, status = https.request 'https://coveralls.io/api/v1/jobs', form_data

		msg = json.decode body

		assert code == 200, string.format "Error updating Coveralls: (status: %s) (response: %s)", status, msg.message
		print msg.url

Coveralls = coveralls!

coverage = require "coveralls.coverage"
require "json"

export ^

-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-- This copyright applies to formencodepart and formencode
formencodepart = (s) ->
	return s and s\gsub "%W", (c) ->
			if c ~= " "
				string.format "%%%02x", c\byte!
			else
				"+"

formencode = (form) ->
	result = {}
	if form[1] -- Array of ordered { name, value }
		for _, field in ipairs form  do
			table.insert result, formencodepart(field.name).."="..formencodepart(field.value)
	else -- Unordered map of name -> value
		for name, value in pairs(form) do
			table.insert result, formencodepart(name).."="..formencodepart(value)

	table.concat result, "&"

class coveralls extends coverage.CodeCoverage
	Travis: "travis-ci"
	Jenkins: "jenkins"
	Semaphore: "semaphore"
	Circle: "circleci"
	Local: "local"
	Debug: "debug"

	new: =>
		@source_files = {}
		-- The continuous integration service
		@service_name = @@Travis
		-- The continuous integration service job id
		@service_job_id = nil
		-- The secret repo token for your repository, found at the bottom of
		-- your repository's page on Coveralls.
		@repo_token = nil

		@dirname = 'src'
		@ext = '*.lua'

		super!

	coverDir: (dirname, ext = '*.lua') =>
		dir = require "pl.dir"
		@cover f for f in *(dir.getfiles dirname, ext)
		@coverDir d, ext for d in *(dir.getdirectories dirname)

	cover: (fname) =>
		file_coverage = @file_coverage fname
		return unless file_coverage
		c = file_coverage.coverage
		length = #c
		for k, v in pairs c
			length = k if k > length

		for i = 1, length
			file_coverage.coverage[i] = json.util.null if c[i] == nil

		table.insert @source_files, file_coverage

	send: =>
		if not @service_job_id
			env = nil
			switch @service_name
				when @@Travis
					env = 'TRAVIS_JOB_ID'
				when @@Jenkins
					env = 'BUILD_NUMBER'
				when @@Semaphore
					env = 'SEMAPHORE_BUILD_NUMBER'
				when @@Circle
					env = 'CIRCLE_BUILD_NUM'
				when @@Local
					return
				when @@Debug
					moon = require "moon"
					moon.p @source_files
					json_data = json.encode {
						service_name: @service_name,
						service_job_id: @service_job_id,
						repo_token: @repo_token,
						source_files: @source_files,
					}
					print #json_data
					form_data = formencode { json: json_data }
					print #form_data
					return

			@service_job_id = os.getenv env

		assert @service_job_id, "A service_job_id must be specified"

		form_data = formencode {
			json: json.encode {
				service_name: @service_name,
				service_job_id: @service_job_id,
				repo_token: @repo_token,
				source_files: @source_files,
			}
		}

		https = require "ssl.https"
		body, code, headers, status = https.request 'https://coveralls.io/api/v1/jobs', form_data

		msg = json.decode body

		assert code == 200, string.format "Error updating Coveralls: (status: %s) (response: %s)", status, msg.message
		print msg.url

Coveralls = coveralls!


Stub utils.Draw

Stub utils.Draw

export ^

class draw
	blendMode = nil
	new: ->
	circle: (x, y, radius, color = 0xFFFFFF) ->
	circlePlus: (x, y, radius, color = 0xFFFFFF, alpha = 1, fill = true, thick = 1) ->
	curve: (x1, y1, x2, y2, x3, y3, thick = 1, color = 0, alpha = 1) ->
	entity: (e, x = 0, y = 0, addEntityPosition = false) ->
	graphic: (g, x = 0, y = 0) ->
	hitbox: (e, outline = true, color = 0xFFFFFF, alpha = 1) ->
	line: (x1, y1, x2, y2, color = 0xFFFFFF) ->
	linePlus: (x1, y1, x2, y2, color = 0xFF000000, alpha = 1, thick = 1) ->
	rect: (x, y, width, height, color = 0xFFFFFF, alpha = 1) ->
	resetTarget: ->
	setTarget: (target, camera = nil, blend = nil) ->
	text: (text, x = 0, y = 0, options = nil) ->

	__drawToScreen = ->

Draw = draw!


Aegisub Example: fix log function invocation.

-- This library is unlicensed under CC0
local requireffi, ffi, looseVersionCompare
versionRecord = '0.7.1'

haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'

if haveDepCtrl
	versionRecord = DependencyControl( {
		name: "SubInspector",
		version: versionRecord,
		description: "Provides low level inspection and analysis of subtitles post-rasterization.",
		author: "torque",
		url: "https://github.com/TypesettingTools/SubInspector",
		moduleName: "SubInspector.Inspector",
		feed: "https://raw.githubusercontent.com/TypesettingTools/SubInspector/master/DependencyControl.json",
		{
			{ "ffi" }
			{ "requireffi.requireffi", version: "0.1.1" }
		}
	} )

	SIVersionCompat = DependencyControl( {
		moduleName: "SubInspector.Compat",
		version: "0.5.1",
		virtual: true
	} )

	looseVersionCompare = ( ASSIVersion ) ->
		siVer = DependencyControl( { moduleName: "SubInspector.Lib", version: ASSIVersion, virtual: true } )
		unless SIVersionCompat\checkVersion siVer, "major"
			return nil, ("Inspector.moon library is too old. Must be v%s.")\format siVer\getVersionString nil, "major"
		unless siVer\checkVersion SIVersionCompat, "minor"
			return nil, ("libSubInspector library is too old. Must be v%s compatible.")\format SIVersionCompat\getVersionString nil, "minor"

		return true

	ffi, requireffi = versionRecord\requireModules!

else
	ffi  = require 'ffi'
	requireffi = require 'requireffi.requireffi'
	SIVersionCompat = 0x000501

	versionComponents = ( version ) ->
		return (version / 65536) % 256, (version / 256) % 256, version % 256

	looseVersionCompare = ( ASSIVersion ) ->
		lmajor, lminor, lpatch = versionComponents ASSIVersion
		smajor, sminor, spatch = versionComponents SIVersionCompat
		if smajor != lmajor
			return nil, ("Major version mismatch. Got %d, wanted %d.")\format lmajor, smajor
		if sminor > lminor
			return nil, ("libSubInspector library is too old. Must be v%d.%d.%d compatible.")\format smajor, sminor, spatch

		return true

ffi.cdef( [[
typedef struct {
	int x, y;
	unsigned int w, h;
	uint32_t hash;
	uint8_t solid;
} SI_Rect;

uint32_t    si_getVersion( void );
const char* si_getErrorString( void* );
void*       si_init( int, int, const char*, const char* );
void        si_changeResolution( void*, int, int );
void        si_reloadFonts( void*, const char*, const char* );
int         si_setHeader( void*, const char*, size_t );
int         si_setScript( void*, const char*, size_t );
int         si_calculateBounds( void*, SI_Rect*, const int32_t*, const uint32_t );
void        si_cleanup( void* );
]] )

SubInspector, libraryPath = requireffi( 'SubInspector.Inspector.SubInspector' )

log = ( message, ... ) ->
	aegisub.log 2, message .. '\n', ...

collectHeader = ( subtitles ) =>
	scriptHeader = {
		"[Script Info]"
	}

	-- These are the only header fields that actually affect the way ASS
	-- is rendered. I don't actually know if ScriptType matters.
	infoFields = {
		PlayResX:   true
		PlayResY:   true
		WrapStyle:  true
		ScriptType: true
		ScaledBorderAndShadow: true
	}

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

	-- This loop assumes the subtitle script is nicely organized in the
	-- order [Script Info] -> [V4+ Styles] -> [Events]. This isn't
	-- guaranteed for scripts floating around in the wild, but fortunately
	-- Aegisub 3.2 seems to do a very good job of ensuring this order when
	-- a script is loaded. In other words, this loop is probably only safe
	-- to do from within automation.
	for index = 1, #subtitles
		with line = subtitles[index]
			if "info" == .class
				if infoFields[.key]
					table.insert scriptHeader, .raw

				if "PlayResX" == .key
					resX = tonumber .value
				elseif "PlayResY" == .key
					resY = tonumber .value

			elseif "style" == .class
				unless seenStyles
					table.insert scriptHeader, "[V4+ Styles]\n"
					seenStyles = true

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

	-- If a video is loaded, use its resolution instead of the script's
	-- resolution. Don't use low-resolution workraws for typesetting!
	vidResX, vidResY = aegisub.video_size!
	if vidResX and (vidResX != resX or vidResY != resY)
		@.logFunc "Script and loaded video resolution mismatch, preferring video."

	@resX = vidResX or resX
	@resY = vidResY or resY

	@header = table.concat scriptHeader, '\n'
	@styles = styles

validateRect = ( rect ) ->
	bounds = {
		x: tonumber rect.x
		y: tonumber rect.y
		w: tonumber rect.w
		h: tonumber rect.h
		hash: tonumber rect.hash
		solid: (rect.solid == 1)
	}

	if bounds.w == 0 or bounds.h == 0
		return false
	else
		return bounds

defaultTimes = ( lines ) ->
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	seenTimes = { }
	times = { }
	hasFrames = ffms 0

	if hasFrames
		times.frames = { }
		for line in *lines
			with line
				for frame = ffms( .start_time ), .si_exhaustive == true and ffms( .end_time ) - 1 or ffms( .start_time )
					frameTime = math.floor 0.5*( msff( frame ) + msff( frame + 1 ) )
					unless seenTimes[frameTime]
						table.insert times.frames, frame
						table.insert times, frameTime
						seenTimes[frameTime] = true
	else
		for line in *lines
			with line
				unless seenTimes[.start_time]
					table.insert times, .start_time
					seenTimes[.start_time] = true

	-- This will only happen if all lines are displayed for 0 frames.
	unless 0 < #times
		return false
	else
		return times

addStyles = ( line, scriptText, seenStyles ) =>
	if @styles[line.style] and not seenStyles[line.style]
		table.insert scriptText, @styles[line.style]
		seenStyles[line.style] = true

	line.text\gsub "{(.-)}", ( tagBlock ) ->
		tagBlock\gsub "\\r([^\\}]*)", ( styleName ) ->
			if 0 < #styleName and @styles[styleName] and not seenStyles[styleName]
				table.insert scriptText, @styles[styleName]
				seenStyles[styleName] = true

class Inspector
	@version = versionRecord

	new: ( subtitles = error( "You must provide the subtitles object." ), fcConfig = libraryPath .. "fonts.conf", fontDir = aegisub.decode_path( '?script/fonts' ), logFunc = log ) =>

		success, message = looseVersionCompare SubInspector.si_getVersion!
		unless success
			return nil, message

		inspectorFree = ( inspector ) -> SubInspector.si_cleanup inspector

		@inspector = ffi.gc SubInspector.si_init( 1, 1, fcConfig, fontDir ), inspectorFree

		assert @inspector, "SubInspector C initialization failed."

		@fontDir = fontDir
		@fcConfig = fcConfig
		@logFunc = logFunc

		success, message = @updateHeader subtitles
		assert success, message

	updateHeader: ( subtitles ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		collectHeader @, subtitles
		SubInspector.si_changeResolution @inspector, @resX, @resY

		if SubInspector.si_setHeader( @inspector, @header, #@header ) != 0
			return nil, "Failed to set header.\n" .. ffi.string SubInspector.si_getErrorString @inspector

		return true

	reloadFonts: ( fcConfig = @fcConfig, fontDir = @fontDir ) =>
		SubInspector.si_reloadFonts @inspector, fcConfig, fontDir
		@fontDir = fontDir
		@fcConfig = fcConfig

	-- Arguments:
	-- subtitles: the subtitles object that Aegisub passes to the macro.

	-- line: an array-like table of line tables that have at least the
	--       fields `start_time`, `text`, `end_time` and `raw`. `raw` is
	--       mandatory in all cases. `start_time` and `end_time` are only
	--       necessary if a table of times to render the line is not
	--       provided and a video is loaded. Allows rendering 1 or more
	--       lines simultaneously.

	-- times: a table of times (in milliseconds) to render the line at.
	--        Defaults to the start time of the line unless there is a video
	--        loaded and the line contains the field line.si_exhaustive =
	--        true, in which case it defaults to every single frame that the
	--        line is displayed.

	-- Returns:
	-- An array-like table of bounding boxes, and an array-like table of
	-- render times. The two tables are the same length. If multiple lines
	-- are passed, the bounding box for a given time is the combined
	-- bounding boxes of all the lines rendered at that time.

	-- Error Handling:
	-- If an error is encountered, getBounds returns nil and an error string,
	-- which is typical for lua error reporting. In order to distinguish
	-- between an error and a valid false return, users should make sure
	-- they actually compare result to nil and false, rather than just
	-- checking that the result is not falsy.

	getBounds: ( lines, times = defaultTimes lines ) =>
		unless times
			return nil, "The render times table was empty."

		for i = 1,#lines
			line = lines[i]
			if nil == line.raw
				if line.createRaw
					line\createRaw!
				else
					return nil, "line.raw is missing from line #{i}."

		scriptText = { }
		seenStyles = { }
		if @styles.Default
			seenStyles.Default = true
			table.insert scriptText, @styles.Default

		for line in *lines
			addStyles @, line, scriptText, seenStyles

		table.insert scriptText, '[Events]'

		for line in *lines
			table.insert scriptText, line.raw

		scriptString = table.concat scriptText, '\n'
		if 0 < SubInspector.si_setScript @inspector, scriptString, #scriptString
			return nil, "Could not set script" .. ffi.string SubInspector.si_getErrorString @inspector

		renderCount = #times
		cTimes = ffi.new 'int32_t[?]', renderCount
		cRects = ffi.new 'SI_Rect[?]', renderCount

		for i = 0, renderCount - 1
			cTimes[i] = times[i + 1]

		if 0 < SubInspector.si_calculateBounds @inspector, cRects, cTimes, renderCount
			return nil, "Error calculating bounds" .. ffi.string SubInspector.si_getErrorString @inspector

		rects = { }
		for i = 0, renderCount - 1
			table.insert rects, validateRect cRects[i]

		return rects, times

if haveDepCtrl
	return versionRecord\register Inspector
else
	return Inspector

-- This library is unlicensed under CC0
local requireffi, ffi, looseVersionCompare
versionRecord = '0.7.0'

haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'

if haveDepCtrl
	versionRecord = DependencyControl( {
		name: "SubInspector",
		version: versionRecord,
		description: "Provides low level inspection and analysis of subtitles post-rasterization.",
		author: "torque",
		url: "https://github.com/TypesettingTools/SubInspector",
		moduleName: "SubInspector.Inspector",
		feed: "https://raw.githubusercontent.com/TypesettingTools/SubInspector/master/DependencyControl.json",
		{
			{ "ffi" }
			{ "requireffi.requireffi", version: "0.1.1" }
		}
	} )

	SIVersionCompat = DependencyControl( {
		moduleName: "SubInspector.Compat",
		version: "0.5.1",
		virtual: true
	} )

	looseVersionCompare = ( ASSIVersion ) ->
		siVer = DependencyControl( { moduleName: "SubInspector.Lib", version: ASSIVersion, virtual: true } )
		unless SIVersionCompat\checkVersion siVer, "major"
			return nil, ("Inspector.moon library is too old. Must be v%s.")\format siVer\getVersionString nil, "major"
		unless siVer\checkVersion SIVersionCompat, "minor"
			return nil, ("libSubInspector library is too old. Must be v%s compatible.")\format SIVersionCompat\getVersionString nil, "minor"

		return true

	ffi, requireffi = versionRecord\requireModules!

else
	ffi  = require 'ffi'
	requireffi = require 'requireffi.requireffi'
	SIVersionCompat = 0x000501

	versionComponents = ( version ) ->
		return (version / 65536) % 256, (version / 256) % 256, version % 256

	looseVersionCompare = ( ASSIVersion ) ->
		lmajor, lminor, lpatch = versionComponents ASSIVersion
		smajor, sminor, spatch = versionComponents SIVersionCompat
		if smajor != lmajor
			return nil, ("Major version mismatch. Got %d, wanted %d.")\format lmajor, smajor
		if sminor > lminor
			return nil, ("libSubInspector library is too old. Must be v%d.%d.%d compatible.")\format smajor, sminor, spatch

		return true

ffi.cdef( [[
typedef struct {
	int x, y;
	unsigned int w, h;
	uint32_t hash;
	uint8_t solid;
} SI_Rect;

uint32_t    si_getVersion( void );
const char* si_getErrorString( void* );
void*       si_init( int, int, const char*, const char* );
void        si_changeResolution( void*, int, int );
void        si_reloadFonts( void*, const char*, const char* );
int         si_setHeader( void*, const char*, size_t );
int         si_setScript( void*, const char*, size_t );
int         si_calculateBounds( void*, SI_Rect*, const int32_t*, const uint32_t );
void        si_cleanup( void* );
]] )

SubInspector, libraryPath = requireffi( 'SubInspector.Inspector.SubInspector' )

log = ( message, ... ) ->
	aegisub.log 2, message .. '\n', ...

collectHeader = ( subtitles ) =>
	scriptHeader = {
		"[Script Info]"
	}

	-- These are the only header fields that actually affect the way ASS
	-- is rendered. I don't actually know if ScriptType matters.
	infoFields = {
		PlayResX:   true
		PlayResY:   true
		WrapStyle:  true
		ScriptType: true
		ScaledBorderAndShadow: true
	}

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

	-- This loop assumes the subtitle script is nicely organized in the
	-- order [Script Info] -> [V4+ Styles] -> [Events]. This isn't
	-- guaranteed for scripts floating around in the wild, but fortunately
	-- Aegisub 3.2 seems to do a very good job of ensuring this order when
	-- a script is loaded. In other words, this loop is probably only safe
	-- to do from within automation.
	for index = 1, #subtitles
		with line = subtitles[index]
			if "info" == .class
				if infoFields[.key]
					table.insert scriptHeader, .raw

				if "PlayResX" == .key
					resX = tonumber .value
				elseif "PlayResY" == .key
					resY = tonumber .value

			elseif "style" == .class
				unless seenStyles
					table.insert scriptHeader, "[V4+ Styles]\n"
					seenStyles = true

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

	-- If a video is loaded, use its resolution instead of the script's
	-- resolution. Don't use low-resolution workraws for typesetting!
	vidResX, vidResY = aegisub.video_size!
	if vidResX and (vidResX != resX or vidResY != resY)
		@logFunc "Script and loaded video resolution mismatch, preferring video."

	@resX = vidResX or resX
	@resY = vidResY or resY

	@header = table.concat scriptHeader, '\n'
	@styles = styles

validateRect = ( rect ) ->
	bounds = {
		x: tonumber rect.x
		y: tonumber rect.y
		w: tonumber rect.w
		h: tonumber rect.h
		hash: tonumber rect.hash
		solid: (rect.solid == 1)
	}

	if bounds.w == 0 or bounds.h == 0
		return false
	else
		return bounds

defaultTimes = ( lines ) ->
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	seenTimes = { }
	times = { }
	hasFrames = ffms 0

	if hasFrames
		times.frames = { }
		for line in *lines
			with line
				for frame = ffms( .start_time ), .si_exhaustive == true and ffms( .end_time ) - 1 or ffms( .start_time )
					frameTime = math.floor 0.5*( msff( frame ) + msff( frame + 1 ) )
					unless seenTimes[frameTime]
						table.insert times.frames, frame
						table.insert times, frameTime
						seenTimes[frameTime] = true
	else
		for line in *lines
			with line
				unless seenTimes[.start_time]
					table.insert times, .start_time
					seenTimes[.start_time] = true

	-- This will only happen if all lines are displayed for 0 frames.
	unless 0 < #times
		return false
	else
		return times

addStyles = ( line, scriptText, seenStyles ) =>
	if @styles[line.style] and not seenStyles[line.style]
		table.insert scriptText, @styles[line.style]
		seenStyles[line.style] = true

	line.text\gsub "{(.-)}", ( tagBlock ) ->
		tagBlock\gsub "\\r([^\\}]*)", ( styleName ) ->
			if 0 < #styleName and @styles[styleName] and not seenStyles[styleName]
				table.insert scriptText, @styles[styleName]
				seenStyles[styleName] = true

class Inspector
	@version = versionRecord

	new: ( subtitles = error( "You must provide the subtitles object." ), fcConfig = libraryPath .. "fonts.conf", fontDir = aegisub.decode_path( '?script/fonts' ), logFunc = log ) =>

		success, message = looseVersionCompare SubInspector.si_getVersion!
		unless success
			return nil, message

		inspectorFree = ( inspector ) -> SubInspector.si_cleanup inspector

		@inspector = ffi.gc SubInspector.si_init( 1, 1, fcConfig, fontDir ), inspectorFree

		assert @inspector, "SubInspector C initialization failed."

		@fontDir = fontDir
		@fcConfig = fcConfig
		@logFunc = logFunc

		success, message = @updateHeader subtitles
		assert success, message

	updateHeader: ( subtitles ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		collectHeader @, subtitles
		SubInspector.si_changeResolution @inspector, @resX, @resY

		if SubInspector.si_setHeader( @inspector, @header, #@header ) != 0
			return nil, "Failed to set header.\n" .. ffi.string SubInspector.si_getErrorString @inspector

		return true

	reloadFonts: ( fcConfig = @fcConfig, fontDir = @fontDir ) =>
		SubInspector.si_reloadFonts @inspector, fcConfig, fontDir
		@fontDir = fontDir
		@fcConfig = fcConfig

	-- Arguments:
	-- subtitles: the subtitles object that Aegisub passes to the macro.

	-- line: an array-like table of line tables that have at least the
	--       fields `start_time`, `text`, `end_time` and `raw`. `raw` is
	--       mandatory in all cases. `start_time` and `end_time` are only
	--       necessary if a table of times to render the line is not
	--       provided and a video is loaded. Allows rendering 1 or more
	--       lines simultaneously.

	-- times: a table of times (in milliseconds) to render the line at.
	--        Defaults to the start time of the line unless there is a video
	--        loaded and the line contains the field line.si_exhaustive =
	--        true, in which case it defaults to every single frame that the
	--        line is displayed.

	-- Returns:
	-- An array-like table of bounding boxes, and an array-like table of
	-- render times. The two tables are the same length. If multiple lines
	-- are passed, the bounding box for a given time is the combined
	-- bounding boxes of all the lines rendered at that time.

	-- Error Handling:
	-- If an error is encountered, getBounds returns nil and an error string,
	-- which is typical for lua error reporting. In order to distinguish
	-- between an error and a valid false return, users should make sure
	-- they actually compare result to nil and false, rather than just
	-- checking that the result is not falsy.

	getBounds: ( lines, times = defaultTimes lines ) =>
		unless times
			return nil, "The render times table was empty."

		for i = 1,#lines
			line = lines[i]
			if nil == line.raw
				if line.createRaw
					line\createRaw!
				else
					return nil, "line.raw is missing from line #{i}."

		scriptText = { }
		seenStyles = { }
		if @styles.Default
			seenStyles.Default = true
			table.insert scriptText, @styles.Default

		for line in *lines
			addStyles @, line, scriptText, seenStyles

		table.insert scriptText, '[Events]'

		for line in *lines
			table.insert scriptText, line.raw

		scriptString = table.concat scriptText, '\n'
		if 0 < SubInspector.si_setScript @inspector, scriptString, #scriptString
			return nil, "Could not set script" .. ffi.string SubInspector.si_getErrorString @inspector

		renderCount = #times
		cTimes = ffi.new 'int32_t[?]', renderCount
		cRects = ffi.new 'SI_Rect[?]', renderCount

		for i = 0, renderCount - 1
			cTimes[i] = times[i + 1]

		if 0 < SubInspector.si_calculateBounds @inspector, cRects, cTimes, renderCount
			return nil, "Error calculating bounds" .. ffi.string SubInspector.si_getErrorString @inspector

		rects = { }
		for i = 0, renderCount - 1
			table.insert rects, validateRect cRects[i]

		return rects, times

if haveDepCtrl
	return versionRecord\register Inspector
else
	return Inspector