Fixed floating image panels

Fixed floating image panels

--
-- Copyright (C) 2017 DBot
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

net.Receive 'PPM2.DamageAnimation', ->
    ent = net.ReadEntity()
    return if not IsValid(ent) or not ent\IsPlayer()
    hook.Call('PPM2_HurtAnimation', nil, ent)

net.Receive 'PPM2.KillAnimation', ->
    ent = net.ReadEntity()
    return if not IsValid(ent) or not ent\IsPlayer()
    hook.Call('PPM2_KillAnimation', nil, ent)

net.Receive 'PPM2.AngerAnimation', ->
    ent = net.ReadEntity()
    return if not IsValid(ent) or not ent\IsPlayer()
    hook.Call('PPM2_AngerAnimation', nil, ent)

net.Receive 'PPM2.PlayEmote', ->
    emoteID = net.ReadUInt(8)
    ply = net.ReadEntity()
    return if not IsValid(ply) or not ply\IsPlayer()
    return if not PPM2.AVALIABLE_EMOTES[emoteID]
    hook.Call('PPM2_EmoteAnimation', nil, ply, PPM2.AVALIABLE_EMOTES[emoteID].sequence, PPM2.AVALIABLE_EMOTES[emoteID].time)

PPM2.EmotesPanelContext\Remove() if IsValid(PPM2.EmotesPanelContext)
PPM2.EmotesPanel\Remove() if IsValid(PPM2.EmotesPanel)

CONSOLE_EMOTES_COMMAND = (ply = LocalPlayer(), cmd = '', args = {}) ->
    args[1] = args[1] or ''
    emoteID = tonumber(args[1])

    if emoteID
        if not PPM2.AVALIABLE_EMOTES[emoteID]
            PPM2.Message('No such emotion with ID: ', emoteID)
            return
        net.Start('PPM2.PlayEmote')
        net.WriteUInt(emoteID, 8)
        net.SendToServer()
        hook.Call('PPM2_EmoteAnimation', nil, LocalPlayer(), PPM2.AVALIABLE_EMOTES[emoteID].sequence, PPM2.AVALIABLE_EMOTES[emoteID].time)
    else
        emoteID = args[1]\lower()
        if not PPM2.AVALIABLE_EMOTES_BY_SEQUENCE[emoteID]
            PPM2.Message('No such emotion with ID: ', emoteID)
            return
        net.Start('PPM2.PlayEmote')
        net.WriteUInt(PPM2.AVALIABLE_EMOTES_BY_SEQUENCE[emoteID].id, 8)
        net.SendToServer()
        hook.Call('PPM2_EmoteAnimation', nil, LocalPlayer(), emoteID, PPM2.AVALIABLE_EMOTES_BY_SEQUENCE[emoteID].time)

CONSOLE_DEF_LIST = ['ppm2_emote "' .. sequence .. '"' for {:sequence} in *PPM2.AVALIABLE_EMOTES]
CONSOLE_EMOTES_AUTOCOMPLETE = (cmd = '', args = '') ->
    args = args\Trim()
    return CONSOLE_DEF_LIST if args == ''
    output = {}
    for {:sequence} in *PPM2.AVALIABLE_EMOTES
        if string.find(sequence, '^' .. args)
            table.insert(output, 'ppm2_emote "' .. sequence .. '"')
    return output

concommand.Add 'ppm2_emote', CONSOLE_EMOTES_COMMAND, CONSOLE_EMOTES_AUTOCOMPLETE

BUTTON_DRAW_FUNC = (w = 0, h = 0) =>
    @hoverDelta = math.Clamp(@hoverDelta + (@IsHovered() and FrameTime() or -FrameTime()) * 5, 0, 1)
    col = @hoverDelta * 150
    col = 200 if @IsDown()
    surface.SetDrawColor(col, col, col, 150)
    surface.DrawRect(0, 0, w, h)

BUTTON_CLICK_FUNC = =>
    net.Start('PPM2.PlayEmote')
    net.WriteUInt(@id, 8)
    net.SendToServer()
    hook.Call('PPM2_EmoteAnimation', nil, LocalPlayer(), @sequence, @time)

BUTTON_TEXT_COLOR = Color(255, 255, 255)

IMAGE_PANEL_THINK = =>
    @lastThink = RealTime() + .4
    if @IsHovered()
        if not @oldHover
            @oldHover = true
            @hoverPnl\SetVisible(true)
            x, y = @LocalToScreen(0, 0)
            @hoverPnl\SetPos(x - 256, y - 224)
    else
        if @oldHover
            @oldHover = false
            @hoverPnl\SetVisible(false)
HOVERED_IMAGE_PANEL_THINK = =>
    if not @parent\IsValid()
        @Remove()
        return
    if @parent.lastThink < RealTime()
        @SetVisible(false)
CreatePanel = (parent) ->
    self = vgui.Create('DPanel', parent)
    @SetSize(200, 300)
    @Paint = (w = 0, h = 0) =>
        surface.SetDrawColor(0, 0, 0, 150)
        surface.DrawRect(0, 0, w, h)
    @scroll = vgui.Create('DScrollPanel', @)
    with @scroll
        \Dock(FILL)
        \SetSize(200, 300)
        .Paint = ->
        \SetMouseInputEnabled(true)
    @buttons = for {:name, :id, :sequence, :time, :fexists, :filecrop} in *PPM2.AVALIABLE_EMOTES
        btn = vgui.Create('DButton', @scroll)
        with btn
            \SetTextColor(BUTTON_TEXT_COLOR)
            .Paint = BUTTON_DRAW_FUNC
            .id = id
            .time = time
            .sequence = sequence
            .hoverDelta = 0
            .DoClick = BUTTON_CLICK_FUNC
            \SetSize(200, 32)
            \SetText(name)
            \SetFont('HudHintTextLarge')
            \Dock(TOP)
            if fexists
                image = vgui.Create('DImage', btn)
                with image
                    \Dock(LEFT)
                    \SetSize(32, 32)
                    \SetImage(filecrop)
                    \SetMouseInputEnabled(true)
                    .hoverPnl = vgui.Create('DImage')
                    .Think = IMAGE_PANEL_THINK
                    .oldHover = false
                    with .hoverPnl
                        \SetMouseInputEnabled(false)
                        \SetVisible(false)
                        \SetImage(filecrop)
                        \SetSize(256, 256)
                        .Think = HOVERED_IMAGE_PANEL_THINK
                        .parent = image
                    .OnRemove = -> .hoverPnl\Remove() if IsValid(.hoverPnl)
        btn
    @scroll\AddItem(btn) for btn in *@buttons
    @SetVisible(false)
    @SetMouseInputEnabled(false)
    return @

hook.Add 'ContextMenuCreated', 'PPM2.Emotes', =>
    PPM2.EmotesPanelContext\Remove() if IsValid(PPM2.EmotesPanelContext)
    PPM2.EmotesPanelContext = CreatePanel(@)
    PPM2.EmotesPanelContext\SetPos(ScrW() / 2 - 100, ScrH() - 300)
    PPM2.EmotesPanelContext\SetVisible(true)
    PPM2.EmotesPanelContext\SetMouseInputEnabled(true)
    timer.Create 'PPM2.ContextMenuEmotesUpdate', 1, 0, ->
        if not IsValid(PPM2.EmotesPanelContext)
            timer.Remove 'PPM2.ContextMenuEmotesUpdate'
            return
        return if not IsValid(LocalPlayer())
        status = LocalPlayer()\IsPony()
        PPM2.EmotesPanelContext\SetVisible(status)
        PPM2.EmotesPanelContext\SetMouseInputEnabled(status)

hook.Add 'StartChat', 'PPM2.Emotes', ->
    if not IsValid(PPM2.EmotesPanel)
        PPM2.EmotesPanel = CreatePanel()
        PPM2.EmotesPanel\SetPos(ScrW() - 500, ScrH() - 300)

    if IsValid(PPM2.EmotesPanel) and LocalPlayer()\IsPony()
        PPM2.EmotesPanel\SetVisible(true)
        PPM2.EmotesPanel\SetMouseInputEnabled(true)
        PPM2.EmotesPanel\RequestFocus()
hook.Add 'FinishChat', 'PPM2.Emotes', ->
    if IsValid(PPM2.EmotesPanel)
        PPM2.EmotesPanel\KillFocus()
        PPM2.EmotesPanel\SetVisible(false)
        PPM2.EmotesPanel\SetMouseInputEnabled(false)

--
-- Copyright (C) 2017 DBot
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

net.Receive 'PPM2.DamageAnimation', ->
    ent = net.ReadEntity()
    return if not IsValid(ent) or not ent\IsPlayer()
    hook.Call('PPM2_HurtAnimation', nil, ent)

net.Receive 'PPM2.KillAnimation', ->
    ent = net.ReadEntity()
    return if not IsValid(ent) or not ent\IsPlayer()
    hook.Call('PPM2_KillAnimation', nil, ent)

net.Receive 'PPM2.AngerAnimation', ->
    ent = net.ReadEntity()
    return if not IsValid(ent) or not ent\IsPlayer()
    hook.Call('PPM2_AngerAnimation', nil, ent)

net.Receive 'PPM2.PlayEmote', ->
    emoteID = net.ReadUInt(8)
    ply = net.ReadEntity()
    return if not IsValid(ply) or not ply\IsPlayer()
    return if not PPM2.AVALIABLE_EMOTES[emoteID]
    hook.Call('PPM2_EmoteAnimation', nil, ply, PPM2.AVALIABLE_EMOTES[emoteID].sequence, PPM2.AVALIABLE_EMOTES[emoteID].time)

PPM2.EmotesPanelContext\Remove() if IsValid(PPM2.EmotesPanelContext)
PPM2.EmotesPanel\Remove() if IsValid(PPM2.EmotesPanel)

CONSOLE_EMOTES_COMMAND = (ply = LocalPlayer(), cmd = '', args = {}) ->
    args[1] = args[1] or ''
    emoteID = tonumber(args[1])

    if emoteID
        if not PPM2.AVALIABLE_EMOTES[emoteID]
            PPM2.Message('No such emotion with ID: ', emoteID)
            return
        net.Start('PPM2.PlayEmote')
        net.WriteUInt(emoteID, 8)
        net.SendToServer()
        hook.Call('PPM2_EmoteAnimation', nil, LocalPlayer(), PPM2.AVALIABLE_EMOTES[emoteID].sequence, PPM2.AVALIABLE_EMOTES[emoteID].time)
    else
        emoteID = args[1]\lower()
        if not PPM2.AVALIABLE_EMOTES_BY_SEQUENCE[emoteID]
            PPM2.Message('No such emotion with ID: ', emoteID)
            return
        net.Start('PPM2.PlayEmote')
        net.WriteUInt(PPM2.AVALIABLE_EMOTES_BY_SEQUENCE[emoteID].id, 8)
        net.SendToServer()
        hook.Call('PPM2_EmoteAnimation', nil, LocalPlayer(), emoteID, PPM2.AVALIABLE_EMOTES_BY_SEQUENCE[emoteID].time)

CONSOLE_DEF_LIST = ['ppm2_emote "' .. sequence .. '"' for {:sequence} in *PPM2.AVALIABLE_EMOTES]
CONSOLE_EMOTES_AUTOCOMPLETE = (cmd = '', args = '') ->
    args = args\Trim()
    return CONSOLE_DEF_LIST if args == ''
    output = {}
    for {:sequence} in *PPM2.AVALIABLE_EMOTES
        if string.find(sequence, '^' .. args)
            table.insert(output, 'ppm2_emote "' .. sequence .. '"')
    return output

concommand.Add 'ppm2_emote', CONSOLE_EMOTES_COMMAND, CONSOLE_EMOTES_AUTOCOMPLETE

BUTTON_DRAW_FUNC = (w = 0, h = 0) =>
    @hoverDelta = math.Clamp(@hoverDelta + (@IsHovered() and FrameTime() or -FrameTime()) * 5, 0, 1)
    col = @hoverDelta * 150
    col = 200 if @IsDown()
    surface.SetDrawColor(col, col, col, 150)
    surface.DrawRect(0, 0, w, h)

BUTTON_CLICK_FUNC = =>
    net.Start('PPM2.PlayEmote')
    net.WriteUInt(@id, 8)
    net.SendToServer()
    hook.Call('PPM2_EmoteAnimation', nil, LocalPlayer(), @sequence, @time)

BUTTON_TEXT_COLOR = Color(255, 255, 255)

IMAGE_PANEL_THINK = =>
    if @IsHovered()
        if not @oldHover
            @oldHover = true
            @hoverPnl\SetVisible(true)
            x, y = @LocalToScreen(0, 0)
            @hoverPnl\SetPos(x - 256, y - 224)
    else
        if @oldHover
            @oldHover = false
            @hoverPnl\SetVisible(false)
CreatePanel = (parent) ->
    self = vgui.Create('DPanel', parent)
    @SetSize(200, 300)
    @Paint = (w = 0, h = 0) =>
        surface.SetDrawColor(0, 0, 0, 150)
        surface.DrawRect(0, 0, w, h)
    @scroll = vgui.Create('DScrollPanel', @)
    with @scroll
        \Dock(FILL)
        \SetSize(200, 300)
        .Paint = ->
        \SetMouseInputEnabled(true)
    @buttons = for {:name, :id, :sequence, :time, :fexists, :filecrop} in *PPM2.AVALIABLE_EMOTES
        btn = vgui.Create('DButton', @scroll)
        with btn
            \SetTextColor(BUTTON_TEXT_COLOR)
            .Paint = BUTTON_DRAW_FUNC
            .id = id
            .time = time
            .sequence = sequence
            .hoverDelta = 0
            .DoClick = BUTTON_CLICK_FUNC
            \SetSize(200, 32)
            \SetText(name)
            \SetFont('HudHintTextLarge')
            \Dock(TOP)
            if fexists
                with vgui.Create('DImage', btn)
                    \Dock(LEFT)
                    \SetSize(32, 32)
                    \SetImage(filecrop)
                    \SetMouseInputEnabled(true)
                    .hoverPnl = vgui.Create('DImage')
                    .Think = IMAGE_PANEL_THINK
                    .oldHover = false
                    with .hoverPnl
                        \SetMouseInputEnabled(false)
                        \SetVisible(false)
                        \SetImage(filecrop)
                        \SetSize(256, 256)
                    .OnRemove = -> .hoverPnl\Remove() if IsValid(.hoverPnl)
        btn
    @scroll\AddItem(btn) for btn in *@buttons
    @SetVisible(false)
    @SetMouseInputEnabled(false)
    return @

hook.Add 'ContextMenuCreated', 'PPM2.Emotes', =>
    PPM2.EmotesPanelContext\Remove() if IsValid(PPM2.EmotesPanelContext)
    PPM2.EmotesPanelContext = CreatePanel(@)
    PPM2.EmotesPanelContext\SetPos(ScrW() / 2 - 100, ScrH() - 300)
    PPM2.EmotesPanelContext\SetVisible(true)
    PPM2.EmotesPanelContext\SetMouseInputEnabled(true)
    timer.Create 'PPM2.ContextMenuEmotesUpdate', 1, 0, ->
        if not IsValid(PPM2.EmotesPanelContext)
            timer.Remove 'PPM2.ContextMenuEmotesUpdate'
            return
        return if not IsValid(LocalPlayer())
        status = LocalPlayer()\IsPony()
        PPM2.EmotesPanelContext\SetVisible(status)
        PPM2.EmotesPanelContext\SetMouseInputEnabled(status)

hook.Add 'StartChat', 'PPM2.Emotes', ->
    if not IsValid(PPM2.EmotesPanel)
        PPM2.EmotesPanel = CreatePanel()
        PPM2.EmotesPanel\SetPos(ScrW() - 500, ScrH() - 300)

    if IsValid(PPM2.EmotesPanel) and LocalPlayer()\IsPony()
        PPM2.EmotesPanel\SetVisible(true)
        PPM2.EmotesPanel\SetMouseInputEnabled(true)
        PPM2.EmotesPanel\RequestFocus()
hook.Add 'FinishChat', 'PPM2.Emotes', ->
    if IsValid(PPM2.EmotesPanel)
        PPM2.EmotesPanel\KillFocus()
        PPM2.EmotesPanel\SetVisible(false)
        PPM2.EmotesPanel\SetMouseInputEnabled(false)


irc: Add typing_counter to determine whether or not the client is still "working"

--- IRC client class
-- @classmod IRCClient

re = require "re"
socket = require 'cqueues.socket'
Logger = require 'logger'

moonscript = {
	errors: require 'moonscript.errors'
}

escapers =  {['s']: ' ', ['r']: '\r', ['n']: '\n', [';']: ';'}

local IRCClient, ContextTable, priority

priority = {
	HIGH: 1,
	DEFAULT: 2,
	LOW: 3,
}

class ContextTable
	-- ::TODO:: allow for multiple *and* index
	get: (name, opts = {})=>
		:multiple, :index, :uses_priority = opts
		multiple = true if multiple == nil
		uses_priority = true if uses_priority == nil

		local output
		output = {} if multiple
		for context, items in pairs(self)
			-- context
			if multiple
				-- add all that exist
				if uses_priority
					for p=priority.HIGH, priority.LOW
						if items[p][name]
							for element in *items[p][name]
								table.insert output, element
				else
					if items[name]
						for element in *items[name]
							table.insert output, element
			else
				-- return the singular if it exists
				if uses_priority
					for p=priority.HIGH, priority.LOW
						return items[p][name] if items[p][name] ~= nil
				else
					return items[name] if items[name] ~= nil

		if multiple
			output
		elseif index
			index\get name, :multiple, :uses_priority

	remove: (name)=>
		for context_name, tbl in pairs(self)
			tbl[name] = nil

class IRCClient
	commands: ContextTable!
	hooks: ContextTable!
	handlers: ContextTable!
	senders: ContextTable!

	__tostring: => "IRCClient"

	default_config = {
		prefix: "!"
	}

	line_pattern = re.compile [[
		-- tags, command, args
		line <- {|
			{:tags: {| (tags sp)? |} :}
			{:prefix: {| (prefix sp)? |} :}
			{:command: (command / numeric) :}
			{:args: {| (sp arg)* |} :} |}
		tags <- '@' tag (';' tag)*
		tag <- {|
			{:is_client: {'+'} :}? -- check: if tag.is_client
			{:vendor: {[^/]+} '/' :}?
			{:key: {[^=; ]+} -> esc_tag :}
			{:value: ('=' {[^; ]+} -> esc_tag) :}?
		|}
		prefix <- ':' (
			{:nick: {[^ !]+} :} '!'
			{:user: {[^ @]+} :} '@'
			{:host: {[^ ]+} :} /
			{:nick: {[^ ]+} :})
		command <- [A-Za-z]+
		numeric <- %d^+3^-4 -- at most four digits, at least three
		arg <- ':' {.+} / {%S+}
		sp <- %s
	]], esc_tag: (tag)-> tag\gsub "\\(.)", setmetatable({
		[":"]: ";"
		s: " "
		r: "\r"
		n: "\n"
	}, __index: (t, k) -> k)

	--- Get a tag based on search parameters (is_client IS NOT matched by key)
	-- @tparam table tags Tag from IRC parser
	-- @tparam table opts Options to search for (is_client: bool, key: string)
	get_tag: (tags, opts)=>
		:is_client, :key = opts
		for tag in *tags
			continue if is_client ~= nil and tag.is_client ~= is_client
			if key ~= nil
				continue if tag.vendor ~= nil and key ~= "#{tag.vendor}/#{tag.key}"
				continue if tag.vendor == nil and key ~= tag.key
			return tag

		return {}

	--- Generate a new IRCClient
	-- @tparam string server IRC server name
	-- @tparam number port IRC port number
	-- @tparam table config Default configuration
	new: (server, port=6697, config)=>
		assert server
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs default_config
			@config[k] = v
		if config
			for k, v in pairs config
				@config[k] = v

		@commands = ContextTable!
		@hooks    = ContextTable!
		@handlers = ContextTable!
		@senders  = ContextTable!
		@server   = {}

	unpack = unpack or table.unpack

	get_priority = (options, fn)->
		return fn and options.priority or priority.DEFAULT

	handle_options: (options, fn)=>
		unless fn
			fn = options
			options = {}

		if options.async
			return (...)->
				args = {...}
				require("queue")\wrap ->
					@pcall_bare fn, unpack args

		if options.wrap_iter
			tmp_fn = coroutine.wrap fn
			tmp_fn!
			return tmp_fn

		fn

	assert_context: (context_table)=>
		assert @context, "Missing @with_context"
		return context_table[assert @context, "Missing @with_context"]

	--- Change the module context for the current command
	-- @tparam string context module context (typically, the name)
	-- @tparam function fn code to run under context
	with_context: (context, fn)=>
		assert @context == nil, "Already in context: #{@context}"
		@context = context

		self["senders"][context] = {} -- no priority for senders, only one
		for key in *{"hooks", "handlers", "commands"}
			self[key][context] = [{} for p=priority.HIGH, priority.LOW]

		fn self, context
		@context = nil

	--- Add an IRC bot command
	-- @tparam string name Bot command name
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function command Function for handling command
	add_command: (name, options, command)=>
		final_command = command
		commands = @assert_context @commands

		-- `options` is passed, which fills in `command`
		if command
			-- is async, send typing until completed
			if options.async
				-- async, send @+draft/typing
				final_command = (prefix, target, line, ...)=>
					@vars.typing_counter = 0 if not @vars.typing_counter
					@vars.typing_counter += 1
					@send 'TAGMSG', target, "+draft/typing": 'active'
					@pcall command, prefix, target, line, ...
					@vars.typing_counter -= 1
					print @vars.typing_counter
					if @vars.typing_counter == 0
						@send 'TAGMSG', target, "+draft/typing": 'done'
					else
						@send 'TAGMSG', target, "+draft/typing": 'active'

		p = get_priority options, final_command
		commands[p][name] = @handle_options options, final_command

	--- Add a client processing hook
	-- @tparam string name Name of event to hook into
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function hook Processor for hook event
	add_hook: (name, options, hook)=>
		hooks = @assert_context @hooks

		p = get_priority options, hook

		unless hooks[p][name]
			hooks[p][name] = {@handle_options options, hook}
		else
			table.insert hooks[p][name], @handle_options(options, hook)

	--- Add an IRC command handler
	-- @tparam string id IRC command ID (numerics MUST be strings)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function handler IRC command processor
	add_handler: (id, options, handler)=>
		handlers = @assert_context @handlers

		p = get_priority options, handler

		unless handlers[p][id]
			handlers[p][id] = {@handle_options options, handler}
		else
			table.insert hooks[p][name], @handle_options(options, handler)

	--- Add an IRC command sending handler, does NOT take a priority
	-- @tparam string id IRC command ID ("PRIVMSG", "JOIN", etc.)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function sender Function to handle message to be sent
	add_sender: (id, options, sender)=>
		senders = @assert_context @senders
		senders[id] = @handle_options(options, sender)

	--- Reset all modules in the IRCClient
	clear_modules: ()=>
		@senders = {}
		@handlers = {}
		@hooks = {}
		@commands = {}

	--- Connect to the IRC server specified in the configuration
	connect: ()=>
		if @socket
			Logger.debug "Shutting down socket: #{tostring(@socket)}"
			@socket\shutdown!
		host = @config.server
		port = @config.port
		ssl  = @config.ssl
		debug_msg = ('Connecting... {host: "%s", port: "%s"}')\format host, port

		@config.nick = 'Moon-Moon' if not @config.nick
		@config.username = 'Mooooon' if not @config.username
		@config.realname = 'Moon Moon: MoonScript IRC Bot' if not @config.realname

		Logger.debug debug_msg, Logger.level.warn .. '--- Connecting...'
		@socket = assert socket.connect{:host, :port}
		if ssl
			Logger.debug 'Starting TLS exchange...'
			@socket\starttls!
			Logger.debug 'Started TLS exchange'
		Logger.print Logger.level.okay .. '--- Connected'
		@fire_hook 'CONNECT'
		nick = @config.nick
		user = @config.username
		real = @config.realname
		pass = @config.password
		Logger.print Logger.level.warn .. '--- Sending authentication data'
		@send_raw 'NICK', nick
		if pass and ssl
			Logger.debug '*** Sending password'
			@send_raw 'PASS', pass
		elseif pass
			Logger.print Logger.level.error .. '*** Not sending password: TLS not enabled ***'
		@send_raw 'USER', user, '*', '*', real
		debug_msg = ('Sent authentication data: {nickname: %s, username: %s, realname: %s}')\format nick, user, real
		Logger.debug debug_msg, Logger.level.okay .. '--- Sent authentication data'

	--- Disconnect from the current IRC server
	disconnect: ()=>
		@socket\shutdown! if @socket
		@fire_hook 'DISCONNECT'

	tag_escapes = {
		";": ":"
		" ": "s"
		"\r": "r"
		"\n": "n"
	}

	--- Serialize a value for being sent in a tag
	serialize_tag_string = (key, value)->
		return key if type(value) == "boolean"
		return key .. "=" .. value\gsub "([: \r\n])", (value)-> "\\#{tbl[value]}"

	--- Serialize client->client tags for sending to the server
	-- @param tags Table of tags to serialize
	serialize_tags = (tags)->
		output = [serialize_tag_string(k, v) for k, v in pairs tags]
		return "@#{table.concat output, ';'}"

	--- Send a raw line to the currently connected IRC server
	-- @param ... List of strings, concatenated using spaces
	send_raw: (...)=>
		output = {...}
		arg_count = select("#", ...)
		opts_table = select(arg_count, ...)
		if type(opts_table) == "table"
			-- we have tags, server accepts tags
			if opts_table.tags and next(opts_table.tags) and @server.ircv3_caps["message-tags"]
				table.insert output, 1, serialize_tags opts_table.tags
			-- remove opts_table regardless of caps
			table.remove output, #output
		arg_count = #output
		if output[arg_count]\find " "
			output[arg_count] = ":#{output[arg_count]}"
		@socket\write table.concat(output, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat output, ' '

	--- Send a command using a builtin sender configured with @add_sender
	-- @tparam string name Name of sender to use
	-- @param ... List of arguments to be passed to sender
	-- @see IRCClient\add_sender
	send: (name, ...)=>
		sender = @senders\get name, multiple: false, index: IRCClient.senders, uses_priority: false
		input = sender self, ...
		@socket\write input if input

	date_pattern: "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+).(%d+)Z"

	--- Parse the time of an ISO 8601 compliant string
	-- @tparam string date ISO 8601 compliant YYYY-MM-DDThh:mm:ssZ date
	-- @treturn number Seconds from Epoch from configured date
	parse_time: (date)=>
		year, month, day, hour, min, sec, mil = date\match @date_pattern
		return os.time(:year, :month, :day, :hour, :min, :sec) + tonumber(mil) / 1000

	--- parse an IRC command using line_pattern
	parse: (line)=> line_pattern\match line

	--- Activate hooks configured for the event name
	-- @tparam string hook_name Name of event to "fire"
	-- @treturn boolean True if no errors were encountered, false otherwise
	-- @treturn table Table containing list of errors from hooks
	fire_hook: (hook_name, ...)=>
		Logger.debug "#{Logger.level.warn}--- Running hooks for #{hook_name}"
		has_run = false
		has_errors = false
		errors = {}

		for _, hook in pairs IRCClient.hooks\get hook_name
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running global hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)
		for _, hook in pairs @hooks\get hook_name
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)

		Logger.debug Logger.level.error .. "*** Hook not found for #{command}" unless has_run

		return has_errors, errors, has_run

	process_line: (line, opts={})=>
		{:tags, :prefix, :command, :args} = @parse line
		opts.line = line
		@process prefix, command, args, tags, opts

	--- Run handlers for an unparsed command
	-- @tparam table prefix Prefix argument from server (nick, user, host)
	-- @tparam table args Argument list from server, including trailing
	-- @tparam table tags Tags list from server
	-- @tparam table opts Optional arguments, in_batch: IRCv3 batching
	process: (prefix, command, args, tags, opts={})=>
		{:in_batch} = opts
		{:nick, :user, :host} = prefix
		if opts.line
			Logger.debug Logger.level.warn .. '--- | Line: ' .. opts.line
			Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
			if nick and user and host
				Logger.debug "#{Logger.level.okay}--- |\\ User: #{nick}!#{user}@#{host}"
			elseif nick
				Logger.debug "#{Logger.level.okay}--- |\\ Nick: #{nick}"
			if #args > 0
				Logger.debug "#{Logger.level.okay}--- |\\ Arguments: #{table.concat args, ' '}"

		has_run = false

		if not in_batch and tags.batch
			-- will return _, _, true if a hook run; this is necessary because
			-- the protocol might send batches even if we don't support them.
			return unless select(2, @fire_hook "BATCH.#{tags.batch}", prefix,
				args, tags, opts)

		for handler in *IRCClient.handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		for handler in *@handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		Logger.debug Logger.level.error .. "*** Handler not found for #{command}" unless has_run

	--- Iterate over lines from a server and handle errors appropriately
	loop: ()=>
		for received_line in @socket\lines! do
			@pcall @process_line, received_line, in_batch: false

	--- Call a function and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, self, ...
		if not ok
			Logger.debug "Arguments:"
			for arg in *{...}
				Logger.debug tostring arg
		return ok, err

	--- Call a function without self and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall_bare: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, ...
		if not ok
			Logger.debug "Arguments:"
			Logger.debug tostring arg for arg in *{...}
		return ok, err

	--- Print a traceback using the internal logging mechanism
	-- @see IRCClient\log
	log_traceback: (err)=>
		err = tostring err
		Logger.debug moonscript.errors.rewrite_traceback debug.traceback!, err
		Logger.debug "#{Logger.level.error} ---"
		return err

	--- Log message from IRC server (used in plugins)
	-- @tparam string input Line to print, IRC color formatted
	-- @see logger
	log: (input)=>
		for line in input\gmatch "[^\r\n]+"
			Logger.print '\00311(\003' .. (@server.caps and
				@server.caps['NETWORK'] or @config.server) ..
				"\00311)\003 #{line}"

return {:IRCClient, :priority}

--- IRC client class
-- @classmod IRCClient

re = require "re"
socket = require 'cqueues.socket'
Logger = require 'logger'

moonscript = {
	errors: require 'moonscript.errors'
}

escapers =  {['s']: ' ', ['r']: '\r', ['n']: '\n', [';']: ';'}

local IRCClient, ContextTable, priority

priority = {
	HIGH: 1,
	DEFAULT: 2,
	LOW: 3,
}

class ContextTable
	-- ::TODO:: allow for multiple *and* index
	get: (name, opts = {})=>
		:multiple, :index, :uses_priority = opts
		multiple = true if multiple == nil
		uses_priority = true if uses_priority == nil

		local output
		output = {} if multiple
		for context, items in pairs(self)
			-- context
			if multiple
				-- add all that exist
				if uses_priority
					for p=priority.HIGH, priority.LOW
						if items[p][name]
							for element in *items[p][name]
								table.insert output, element
				else
					if items[name]
						for element in *items[name]
							table.insert output, element
			else
				-- return the singular if it exists
				if uses_priority
					for p=priority.HIGH, priority.LOW
						return items[p][name] if items[p][name] ~= nil
				else
					return items[name] if items[name] ~= nil

		if multiple
			output
		elseif index
			index\get name, :multiple, :uses_priority

	remove: (name)=>
		for context_name, tbl in pairs(self)
			tbl[name] = nil

class IRCClient
	commands: ContextTable!
	hooks: ContextTable!
	handlers: ContextTable!
	senders: ContextTable!

	__tostring: => "IRCClient"

	default_config = {
		prefix: "!"
	}

	line_pattern = re.compile [[
		-- tags, command, args
		line <- {|
			{:tags: {| (tags sp)? |} :}
			{:prefix: {| (prefix sp)? |} :}
			{:command: (command / numeric) :}
			{:args: {| (sp arg)* |} :} |}
		tags <- '@' tag (';' tag)*
		tag <- {|
			{:is_client: {'+'} :}? -- check: if tag.is_client
			{:vendor: {[^/]+} '/' :}?
			{:key: {[^=; ]+} -> esc_tag :}
			{:value: ('=' {[^; ]+} -> esc_tag) :}?
		|}
		prefix <- ':' (
			{:nick: {[^ !]+} :} '!'
			{:user: {[^ @]+} :} '@'
			{:host: {[^ ]+} :} /
			{:nick: {[^ ]+} :})
		command <- [A-Za-z]+
		numeric <- %d^+3^-4 -- at most four digits, at least three
		arg <- ':' {.+} / {%S+}
		sp <- %s
	]], esc_tag: (tag)-> tag\gsub "\\(.)", setmetatable({
		[":"]: ";"
		s: " "
		r: "\r"
		n: "\n"
	}, __index: (t, k) -> k)

	--- Get a tag based on search parameters (is_client IS NOT matched by key)
	-- @tparam table tags Tag from IRC parser
	-- @tparam table opts Options to search for (is_client: bool, key: string)
	get_tag: (tags, opts)=>
		:is_client, :key = opts
		for tag in *tags
			continue if is_client ~= nil and tag.is_client ~= is_client
			if key ~= nil
				continue if tag.vendor ~= nil and key ~= "#{tag.vendor}/#{tag.key}"
				continue if tag.vendor == nil and key ~= tag.key
			return tag

		return {}

	--- Generate a new IRCClient
	-- @tparam string server IRC server name
	-- @tparam number port IRC port number
	-- @tparam table config Default configuration
	new: (server, port=6697, config)=>
		assert server
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs default_config
			@config[k] = v
		if config
			for k, v in pairs config
				@config[k] = v

		@commands = ContextTable!
		@hooks    = ContextTable!
		@handlers = ContextTable!
		@senders  = ContextTable!
		@server   = {}

	unpack = unpack or table.unpack

	get_priority = (options, fn)->
		return fn and options.priority or priority.DEFAULT

	handle_options: (options, fn)=>
		unless fn
			fn = options
			options = {}

		if options.async
			return (...)->
				args = {...}
				require("queue")\wrap ->
					@pcall_bare fn, unpack args

		if options.wrap_iter
			tmp_fn = coroutine.wrap fn
			tmp_fn!
			return tmp_fn

		fn

	assert_context: (context_table)=>
		assert @context, "Missing @with_context"
		return context_table[assert @context, "Missing @with_context"]

	--- Change the module context for the current command
	-- @tparam string context module context (typically, the name)
	-- @tparam function fn code to run under context
	with_context: (context, fn)=>
		assert @context == nil, "Already in context: #{@context}"
		@context = context

		self["senders"][context] = {} -- no priority for senders, only one
		for key in *{"hooks", "handlers", "commands"}
			self[key][context] = [{} for p=priority.HIGH, priority.LOW]

		fn self, context
		@context = nil

	--- Add an IRC bot command
	-- @tparam string name Bot command name
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function command Function for handling command
	add_command: (name, options, command)=>
		final_command = command
		commands = @assert_context @commands

		-- `options` is passed, which fills in `command`
		if command
			-- is async, send typing until completed
			if options.async
				-- async, send @+draft/typing
				final_command = (prefix, target, line, ...)=>
					@send 'TAGMSG', target, "+draft/typing": "active"
					@pcall command, prefix, target, line, ...
					@send 'TAGMSG', target, "+draft/typing": "done"

		p = get_priority options, final_command
		commands[p][name] = @handle_options options, final_command

	--- Add a client processing hook
	-- @tparam string name Name of event to hook into
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function hook Processor for hook event
	add_hook: (name, options, hook)=>
		hooks = @assert_context @hooks

		p = get_priority options, hook

		unless hooks[p][name]
			hooks[p][name] = {@handle_options options, hook}
		else
			table.insert hooks[p][name], @handle_options(options, hook)

	--- Add an IRC command handler
	-- @tparam string id IRC command ID (numerics MUST be strings)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function handler IRC command processor
	add_handler: (id, options, handler)=>
		handlers = @assert_context @handlers

		p = get_priority options, handler

		unless handlers[p][id]
			handlers[p][id] = {@handle_options options, handler}
		else
			table.insert hooks[p][name], @handle_options(options, handler)

	--- Add an IRC command sending handler, does NOT take a priority
	-- @tparam string id IRC command ID ("PRIVMSG", "JOIN", etc.)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function sender Function to handle message to be sent
	add_sender: (id, options, sender)=>
		senders = @assert_context @senders
		senders[id] = @handle_options(options, sender)

	--- Reset all modules in the IRCClient
	clear_modules: ()=>
		@senders = {}
		@handlers = {}
		@hooks = {}
		@commands = {}

	--- Connect to the IRC server specified in the configuration
	connect: ()=>
		if @socket
			Logger.debug "Shutting down socket: #{tostring(@socket)}"
			@socket\shutdown!
		host = @config.server
		port = @config.port
		ssl  = @config.ssl
		debug_msg = ('Connecting... {host: "%s", port: "%s"}')\format host, port

		@config.nick = 'Moon-Moon' if not @config.nick
		@config.username = 'Mooooon' if not @config.username
		@config.realname = 'Moon Moon: MoonScript IRC Bot' if not @config.realname

		Logger.debug debug_msg, Logger.level.warn .. '--- Connecting...'
		@socket = assert socket.connect{:host, :port}
		if ssl
			Logger.debug 'Starting TLS exchange...'
			@socket\starttls!
			Logger.debug 'Started TLS exchange'
		Logger.print Logger.level.okay .. '--- Connected'
		@fire_hook 'CONNECT'
		nick = @config.nick
		user = @config.username
		real = @config.realname
		pass = @config.password
		Logger.print Logger.level.warn .. '--- Sending authentication data'
		@send_raw 'NICK', nick
		if pass and ssl
			Logger.debug '*** Sending password'
			@send_raw 'PASS', pass
		elseif pass
			Logger.print Logger.level.error .. '*** Not sending password: TLS not enabled ***'
		@send_raw 'USER', user, '*', '*', real
		debug_msg = ('Sent authentication data: {nickname: %s, username: %s, realname: %s}')\format nick, user, real
		Logger.debug debug_msg, Logger.level.okay .. '--- Sent authentication data'

	--- Disconnect from the current IRC server
	disconnect: ()=>
		@socket\shutdown! if @socket
		@fire_hook 'DISCONNECT'

	tag_escapes = {
		";": ":"
		" ": "s"
		"\r": "r"
		"\n": "n"
	}

	--- Serialize a value for being sent in a tag
	serialize_tag_string = (key, value)->
		return key if type(value) == "boolean"
		return key .. "=" .. value\gsub "([: \r\n])", (value)-> "\\#{tbl[value]}"

	--- Serialize client->client tags for sending to the server
	-- @param tags Table of tags to serialize
	serialize_tags = (tags)->
		output = [serialize_tag_string(k, v) for k, v in pairs tags]
		return "@#{table.concat output, ';'}"

	--- Send a raw line to the currently connected IRC server
	-- @param ... List of strings, concatenated using spaces
	send_raw: (...)=>
		output = {...}
		arg_count = select("#", ...)
		opts_table = select(arg_count, ...)
		if type(opts_table) == "table"
			-- we have tags, server accepts tags
			if opts_table.tags and next(opts_table.tags) and @server.ircv3_caps["message-tags"]
				table.insert output, 1, serialize_tags opts_table.tags
			-- remove opts_table regardless of caps
			table.remove output, #output
		arg_count = #output
		if output[arg_count]\find " "
			output[arg_count] = ":#{output[arg_count]}"
		@socket\write table.concat(output, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat output, ' '

	--- Send a command using a builtin sender configured with @add_sender
	-- @tparam string name Name of sender to use
	-- @param ... List of arguments to be passed to sender
	-- @see IRCClient\add_sender
	send: (name, ...)=>
		sender = @senders\get name, multiple: false, index: IRCClient.senders, uses_priority: false
		input = sender self, ...
		@socket\write input if input

	date_pattern: "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+).(%d+)Z"

	--- Parse the time of an ISO 8601 compliant string
	-- @tparam string date ISO 8601 compliant YYYY-MM-DDThh:mm:ssZ date
	-- @treturn number Seconds from Epoch from configured date
	parse_time: (date)=>
		year, month, day, hour, min, sec, mil = date\match @date_pattern
		return os.time(:year, :month, :day, :hour, :min, :sec) + tonumber(mil) / 1000

	--- parse an IRC command using line_pattern
	parse: (line)=> line_pattern\match line

	--- Activate hooks configured for the event name
	-- @tparam string hook_name Name of event to "fire"
	-- @treturn boolean True if no errors were encountered, false otherwise
	-- @treturn table Table containing list of errors from hooks
	fire_hook: (hook_name, ...)=>
		Logger.debug "#{Logger.level.warn}--- Running hooks for #{hook_name}"
		has_run = false
		has_errors = false
		errors = {}

		for _, hook in pairs IRCClient.hooks\get hook_name
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running global hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)
		for _, hook in pairs @hooks\get hook_name
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)

		Logger.debug Logger.level.error .. "*** Hook not found for #{command}" unless has_run

		return has_errors, errors, has_run

	process_line: (line, opts={})=>
		{:tags, :prefix, :command, :args} = @parse line
		opts.line = line
		@process prefix, command, args, tags, opts

	--- Run handlers for an unparsed command
	-- @tparam table prefix Prefix argument from server (nick, user, host)
	-- @tparam table args Argument list from server, including trailing
	-- @tparam table tags Tags list from server
	-- @tparam table opts Optional arguments, in_batch: IRCv3 batching
	process: (prefix, command, args, tags, opts={})=>
		{:in_batch} = opts
		{:nick, :user, :host} = prefix
		if opts.line
			Logger.debug Logger.level.warn .. '--- | Line: ' .. opts.line
			Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
			if nick and user and host
				Logger.debug "#{Logger.level.okay}--- |\\ User: #{nick}!#{user}@#{host}"
			elseif nick
				Logger.debug "#{Logger.level.okay}--- |\\ Nick: #{nick}"
			if #args > 0
				Logger.debug "#{Logger.level.okay}--- |\\ Arguments: #{table.concat args, ' '}"

		has_run = false

		if not in_batch and tags.batch
			-- will return _, _, true if a hook run; this is necessary because
			-- the protocol might send batches even if we don't support them.
			return unless select(2, @fire_hook "BATCH.#{tags.batch}", prefix,
				args, tags, opts)

		for handler in *IRCClient.handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		for handler in *@handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		Logger.debug Logger.level.error .. "*** Handler not found for #{command}" unless has_run

	--- Iterate over lines from a server and handle errors appropriately
	loop: ()=>
		for received_line in @socket\lines! do
			@pcall @process_line, received_line, in_batch: false

	--- Call a function and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, self, ...
		if not ok
			Logger.debug "Arguments:"
			for arg in *{...}
				Logger.debug tostring arg
		return ok, err

	--- Call a function without self and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall_bare: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, ...
		if not ok
			Logger.debug "Arguments:"
			Logger.debug tostring arg for arg in *{...}
		return ok, err

	--- Print a traceback using the internal logging mechanism
	-- @see IRCClient\log
	log_traceback: (err)=>
		err = tostring err
		Logger.debug moonscript.errors.rewrite_traceback debug.traceback!, err
		Logger.debug "#{Logger.level.error} ---"
		return err

	--- Log message from IRC server (used in plugins)
	-- @tparam string input Line to print, IRC color formatted
	-- @see logger
	log: (input)=>
		for line in input\gmatch "[^\r\n]+"
			Logger.print '\00311(\003' .. (@server.caps and
				@server.caps['NETWORK'] or @config.server) ..
				"\00311)\003 #{line}"

return {:IRCClient, :priority}


Make Sfx\resume work

Make Sfx\resume work

require "LunoPunk.LP"

export ^

-- Sound effect object used to play embedded sounds.
class Sfx
	-- Creates a sound effect from an embedded source. Store a reference to
	-- this object so that you can play the sound using play() or loop().
	-- @param	source		The embedded sound class to use.
	-- @param	complete	Optional callback function for when the sound finishes playing.
	new: (source, complete = nil) =>
		-- Optional callback function for when the sound finishes playing.
		@complete = complete
		-- @__transform = SoundTransform!
		@__volume = 1
		@__pan = 0
		@__position = 0
		@__type = ""

		-- Sound infromation.
		@__type
		@__volume = 1
		@__pan = 0
		@__filteredVol
		@__filteredPan
		@__sound
		@__channel
		-- @__transform
		@__position = 0
		@__looping

		-- Stored Sound objects.
		@@__sounds = {}
		@@__typePlaying = {}
		-- @@__typeTransforms = {}

		assert source != nil, "Invalid source Sound."

		if type(source) == "string"
			@__sound = love.audio.newSource source, "static"
			@@__sounds[source] = @__sound
		else
			className = source.__class.__name
			@__sound = @__sounds[className]
			if @__sound == nil
				@__sound = source
				@@__sounds[className] = source

	-- Plays the sound once.
	-- @param	vol	   Volume factor, a value from 0 to 1.
	-- @param	pan	   Panning factor, a value from -1 to 1.
	-- @param   loop   If the audio should loop infinitely
	play: (volume = 1, pan = 0, loop = false) =>
		return if @__sound == nil
		@stop! if @playing!
		@__pan = LP.clamp pan, -1, 1
		@__volume = (if volume < 0 then 0 else volume)
		@__filteredPan = LP.clamp @__pan + @getPan(@__type), -1, 1
		@__filteredVol = math.max 0, @__volume * @getVolume @_type
		--@__transform.pan = @__filteredPan
		--@__transform.volume = @__filteredVol
		@__sound\setLooping loop
		@__sound\setVolume @__volume
		@__sound\play!

		if @playing!
			addPlaying @
			--@__channel.addEventListener Event.SOUND_COMPLETE, @\onComplete

		@__looping = loop
		@__position = 0

	-- Plays the sound looping. Will loop continuously until you call stop(), play(), or loop() again.
	-- @param	vol		Volume factor, a value from 0 to 1.
	-- @param	pan		Panning factor, a value from -1 to 1.
	loop: (vol = 1, pan = 0) =>
		@play vol, pan, true

	-- Stops the sound if it is currently playing.
	--
	-- @return If the sound was stopped.
	stop: =>
		return false if not @playing!
		removePlaying @
		@__position = @__sound\tell!
		--@__channel.removeEventListener(Event.SOUND_COMPLETE, @\onComplete)
		--@__channel.stop()
		--@__channel = nil
		@__sound\stop!
		true

	-- Resumes the sound from the position stop() was called on it.
	resume: =>
		@__sound\seek @__position
		@__sound\play!

		if @playing!
			addPlaying @
			-- @__channel.addEventListener(Event.SOUND_COMPLETE, onComplete)

		@__position = 0

	-- @private Event handler for sound completion.
	onComplete = (e = nil) =>
		if @__looping
			@loop @__volume, @__pan
		else
			@stop!

		@__position = 0
		@complete! if @complete != nil

	-- @private Add the sound to the global list.
	addPlaying = =>
		list = {}
		if not @@__typePlaying[@__type]
			@@__typePlaying[@__type] = list
		else
			list = @__typePlaying[@__type]

		table.insert list, @

	-- @private Remove the sound from the global list. */
	removePlaying = =>
		@@__typePlaying[@__type] = nil if (@@__typePlaying[@__type])

	-- Alter the volume factor (a value from 0 to 1) of the sound during playback.
	volume: (value) =>
		return @__volume if value == nil
		value = 0 if value < 0
		return value if @__channel == nil or @__volume == value
		@__volume = LP.clamp value, 0, 1
		filteredVol = value * @getVolume @__type
		filteredVol = 0 if filteredVol < 0
		return value if @__filteredVol == filteredVol
		@__filteredVol, @__transform.volume = filteredVol, filteredVol
		@__channel.soundTransform = @__transform
		@__volume

	-- Alter the panning factor (a value from -1 to 1) of the sound during playback.
	pan: (value) =>
		return @__pan if value == nil
		value = LP.clamp value, -1, 1
		return value if @__channel == nil or @__pan == value
		filteredPan = LP.clamp value + @getPan(@__type), -1, 1
		return value if @__filteredPan == filteredPan
		@__pan = value
		@__filteredPan, @__transform.pan = filteredPan, filteredPan
		@__channel.soundTransform = @__transform
		@__pan

	-- Change the sound type. This an arbitrary string you can use to group
	-- sounds to mute or pan en masse.
	type: (value) =>
		return @__type if value == nil or @__type == value
		if @playing!
			@removePlaying!
			@__type = value
			@addPlaying!
			-- reset, in case type has different global settings
			@pan @pan!
			@volume @volume!
		else
			@__type = value

		value

	-- If the sound is currently playing.
	playing: => @__sound.typeOf and @__sound\typeOf("Source") and not @__sound\isStopped!

	-- Position of the currently playing sound, in seconds.
	position: => (if @playing! then @__channel.position else @__position) / 1000

	-- Length of the sound, in seconds.
	length: => @__sound.length / 1000

	-- Return the global pan for a type.
	--
	-- @param	type	The type to get the pan from.
	--
	-- @return	The global pan for the type.
	getPan: (type) ->
--		if @@__typeTransforms.exists type
--			transform = @@__typeTransforms.get type
--			return if transform != nil then transform.pan else 0

		0

	-- Return the global volume for a type.
	--
	-- @param	type	The type to get the volume from.
	--
	-- @return	The global volume for the type.
	getVolume: (type) ->
--		if @@__typeTransforms.exists type
--			transform = @@__typeTransforms.get type
--			return if transform != nil then transform.volume else 1

		1

	-- Set the global pan for a type. Sfx instances of this type will add
	-- this pan to their own.
	--
	-- @param	type	The type to set.
	-- @param	pan		The pan value.
	setPan: (type, pan) ->
		transform = @__typeTransforms.get type
		if (transform == nil)
			transform = SoundTransform!
			@@__typeTransforms.set(type, transform)

		transform.pan = LP.clamp pan, -1, 1

		if @__typePlaying.exists type
			sfx.pan sfx.pan! for sfx in @@__typePlaying.get type

	-- Set the global volume for a type. Sfx instances of this type will
	-- multiply their volume by this value.
	--
	-- @param	type	The type to set.
	-- @param	volume	The volume value.
	setVolume: (type, volume) ->
		transform = @@__typeTransforms.get(type)

		if transform == nil
			transform = SoundTransform!
			@@__typeTransforms.set type, transform

		transform.volume = (if volume < 0 then 0 else volume)

		if @@__typePlaying.exists type
			sfx.volume sfx.volume! for sfx in @@__typePlaying.get type

require "LunoPunk.LP"

export ^

-- Sound effect object used to play embedded sounds.
class Sfx
	-- Creates a sound effect from an embedded source. Store a reference to
	-- this object so that you can play the sound using play() or loop().
	-- @param	source		The embedded sound class to use.
	-- @param	complete	Optional callback function for when the sound finishes playing.
	new: (source, complete = nil) =>
		-- Optional callback function for when the sound finishes playing.
		@complete = complete
		-- @__transform = SoundTransform!
		@__volume = 1
		@__pan = 0
		@__position = 0
		@__type = ""

		-- Sound infromation.
		@__type
		@__volume = 1
		@__pan = 0
		@__filteredVol
		@__filteredPan
		@__sound
		@__channel
		-- @__transform
		@__position = 0
		@__looping

		-- Stored Sound objects.
		@@__sounds = {}
		@@__typePlaying = {}
		-- @@__typeTransforms = {}

		assert source != nil, "Invalid source Sound."

		if type(source) == "string"
			@__sound = love.audio.newSource source, "static"
			@@__sounds[source] = @__sound
		else
			className = source.__class.__name
			@__sound = @__sounds[className]
			if @__sound == nil
				@__sound = source
				@@__sounds[className] = source

	-- Plays the sound once.
	-- @param	vol	   Volume factor, a value from 0 to 1.
	-- @param	pan	   Panning factor, a value from -1 to 1.
	-- @param   loop   If the audio should loop infinitely
	play: (volume = 1, pan = 0, loop = false) =>
		return if @__sound == nil
		@stop! if @playing!
		@__pan = LP.clamp pan, -1, 1
		@__volume = (if volume < 0 then 0 else volume)
		@__filteredPan = LP.clamp @__pan + @getPan(@__type), -1, 1
		@__filteredVol = math.max 0, @__volume * @getVolume @_type
		--@__transform.pan = @__filteredPan
		--@__transform.volume = @__filteredVol
		@__sound\setLooping loop
		@__sound\setVolume @__volume
		@__sound\play!

		if @playing!
			addPlaying @
			--@__channel.addEventListener Event.SOUND_COMPLETE, @\onComplete

		@__looping = loop
		@__position = 0

	-- Plays the sound looping. Will loop continuously until you call stop(), play(), or loop() again.
	-- @param	vol		Volume factor, a value from 0 to 1.
	-- @param	pan		Panning factor, a value from -1 to 1.
	loop: (vol = 1, pan = 0) =>
		@play vol, pan, true

	-- Stops the sound if it is currently playing.
	--
	-- @return If the sound was stopped.
	stop: =>
		return false if not @playing!
		removePlaying @
		--@__position = @__channel.position
		--@__channel.removeEventListener(Event.SOUND_COMPLETE, @\onComplete)
		--@__channel.stop()
		--@__channel = nil
		@__sound\stop!
		true

	-- Resumes the sound from the position stop() was called on it.
	resume: =>
		@__channel = @__sound.play(@__position, if @__looping then -1 else 0, @__transform)
		if @playing!
			@addPlaying!
			@__channel.addEventListener(Event.SOUND_COMPLETE, onComplete)

		@__position = 0

	-- @private Event handler for sound completion.
	onComplete = (e = nil) =>
		if @__looping
			@loop @__volume, @__pan
		else
			@stop!

		@__position = 0
		@complete! if @complete != nil

	-- @private Add the sound to the global list.
	addPlaying = =>
		list = {}
		if not @@__typePlaying[@__type]
			@@__typePlaying[@__type] = list
		else
			list = @__typePlaying[@__type]

		table.insert list, @

	-- @private Remove the sound from the global list. */
	removePlaying = =>
		@@__typePlaying[@__type] = nil if (@@__typePlaying[@__type])

	-- Alter the volume factor (a value from 0 to 1) of the sound during playback.
	volume: (value) =>
		return @__volume if value == nil
		value = 0 if value < 0
		return value if @__channel == nil or @__volume == value
		@__volume = LP.clamp value, 0, 1
		filteredVol = value * @getVolume @__type
		filteredVol = 0 if filteredVol < 0
		return value if @__filteredVol == filteredVol
		@__filteredVol, @__transform.volume = filteredVol, filteredVol
		@__channel.soundTransform = @__transform
		@__volume

	-- Alter the panning factor (a value from -1 to 1) of the sound during playback.
	pan: (value) =>
		return @__pan if value == nil
		value = LP.clamp value, -1, 1
		return value if @__channel == nil or @__pan == value
		filteredPan = LP.clamp value + @getPan(@__type), -1, 1
		return value if @__filteredPan == filteredPan
		@__pan = value
		@__filteredPan, @__transform.pan = filteredPan, filteredPan
		@__channel.soundTransform = @__transform
		@__pan

	-- Change the sound type. This an arbitrary string you can use to group
	-- sounds to mute or pan en masse.
	type: (value) =>
		return @__type if value == nil or @__type == value
		if @playing!
			@removePlaying!
			@__type = value
			@addPlaying!
			-- reset, in case type has different global settings
			@pan @pan!
			@volume @volume!
		else
			@__type = value

		value

	-- If the sound is currently playing.
	playing: => @__sound.typeOf and @__sound\typeOf("Source") and not @__sound\isStopped!

	-- Position of the currently playing sound, in seconds.
	position: => (if @playing! then @__channel.position else @__position) / 1000

	-- Length of the sound, in seconds.
	length: => @__sound.length / 1000

	-- Return the global pan for a type.
	--
	-- @param	type	The type to get the pan from.
	--
	-- @return	The global pan for the type.
	getPan: (type) ->
--		if @@__typeTransforms.exists type
--			transform = @@__typeTransforms.get type
--			return if transform != nil then transform.pan else 0

		0

	-- Return the global volume for a type.
	--
	-- @param	type	The type to get the volume from.
	--
	-- @return	The global volume for the type.
	getVolume: (type) ->
--		if @@__typeTransforms.exists type
--			transform = @@__typeTransforms.get type
--			return if transform != nil then transform.volume else 1

		1

	-- Set the global pan for a type. Sfx instances of this type will add
	-- this pan to their own.
	--
	-- @param	type	The type to set.
	-- @param	pan		The pan value.
	setPan: (type, pan) ->
		transform = @__typeTransforms.get type
		if (transform == nil)
			transform = SoundTransform!
			@@__typeTransforms.set(type, transform)

		transform.pan = LP.clamp pan, -1, 1

		if @__typePlaying.exists type
			sfx.pan sfx.pan! for sfx in @@__typePlaying.get type

	-- Set the global volume for a type. Sfx instances of this type will
	-- multiply their volume by this value.
	--
	-- @param	type	The type to set.
	-- @param	volume	The volume value.
	setVolume: (type, volume) ->
		transform = @@__typeTransforms.get(type)

		if transform == nil
			transform = SoundTransform!
			@@__typeTransforms.set type, transform

		transform.volume = (if volume < 0 then 0 else volume)

		if @@__typePlaying.exists type
			sfx.volume sfx.volume! for sfx in @@__typePlaying.get type


Realized whene Tween\finish is called

Realized whene Tween\finish is called

* finish is called from Tweener\upadetTweens. Unfortunately, finish is
  file (Tween.moon) local and wouldn't have been callable. I've fixed
  that and removed the UPDATE EventListener.
* Found an issue when Tween\update could still be called on non-active
  tweens. I believe this is an issue not a feature.

require "LunoPunk.LP"
require "LunoPunk.utils.EventListener"
require "LunoPunk.tweens.TweenEvent"

export ^

TweenType = {
	Persist: "Persist"
	Looping: "Looping"
	OneShot: "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
			AddEventListener TweenEvent.FINISH, complete

	-- Updates the Tween, called by World.
	update: =>
		-- Only update the Tween if active
		return if not @active
		@__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 = 0
			@__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 @ if @__parent != nil

		@__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 "LunoPunk.LP"
require "LunoPunk.utils.EventListener"
require "LunoPunk.tweens.TweenEvent"

export ^

TweenType = {
	Persist: "Persist"
	Looping: "Looping"
	OneShot: "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

		-- I don't understand how the fuck HaxePunk's TweenEvent.UPDATE is ever
		-- called, but I'm going to define a function here
		AddEventListener TweenEvent.UPDATE, ->
			finish @ if @__finish

		if complete != nil
			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 = 0
			@__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 @ if @__parent != nil

		@__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


Fix extra space being added after chat prefix.

-- Copyright (c) 2015 by Adam Hellberg.
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.

NAME, T = ...

MARKOV_PREFIX = '[Markov]'
MARKOV_PATTERN = '^%[Markov%]'

T.chat =
    lastreply: 0

import random from math
import markov, chat from T

REPLY_TIME_LOW = 300
REPLY_TIME_HIGH = 1800

chat.send = (msg, channel = 'GUILD', target) =>
    SendChatMessage('%s %s'\format(MARKOV_PREFIX, msg), channel, nil, target)

chat.receive = (channel, msg, sender) =>
    return if msg\match MARKOV_PATTERN
    if channel == 'GUILD'
        @.lastreply = time() if @.lastreply == 0
        if msg\match '^!markov'
            markov\reply msg\match('^!markov (.+)'), 'GUILD'
        else if time! - @.lastreply > random(REPLY_TIME_LOW, REPLY_TIME_HIGH)
            success = markov\reply msg, 'GUILD', true
            @.lastreply = time! if success

    markov\save msg unless msg\match '^!markov' or msg\match MARKOV_PATTERN

-- Copyright (c) 2015 by Adam Hellberg.
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.

NAME, T = ...

MARKOV_PREFIX = '[Markov] '
MARKOV_PATTERN = '^%[Markov%] '

T.chat =
    lastreply: 0

import random from math
import markov, chat from T

REPLY_TIME_LOW = 300
REPLY_TIME_HIGH = 1800

chat.send = (msg, channel = 'GUILD', target) =>
    SendChatMessage('%s %s'\format(MARKOV_PREFIX, msg), channel, nil, target)

chat.receive = (channel, msg, sender) =>
    return if msg\match MARKOV_PATTERN
    if channel == 'GUILD'
        @.lastreply = time() if @.lastreply == 0
        if msg\match '^!markov'
            markov\reply msg\match('^!markov (.+)'), 'GUILD'
        else if time! - @.lastreply > random(REPLY_TIME_LOW, REPLY_TIME_HIGH)
            success = markov\reply msg, 'GUILD', true
            @.lastreply = time! if success

    markov\save msg unless msg\match '^!markov' or msg\match MARKOV_PATTERN


rss: run polling less often, some cleanup

Former-commit-id: 8fb7b4af859a7b2135b5e4053ec9e8c98f097671
Former-commit-id: f3d2159fd7a2b1dd4def0feabb7a55245b2e6647

-- Module for subscribing to RSS and announce to channels
feedparser = require 'feedparser' -- http://feedparser.luaforge.net/
{:simplehttp, :json, :urlEncode, :bold} = require'util'
html2unicode = require 'html'
sql = require'lsqlite3'

moduleName = 'rss'

get_db = ->
  db = sql.open 'cache/rss.sql'

  code = db\exec [[
    CREATE TABLE IF NOT EXISTS feed (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT,
      rssurl VARCHAR(1024) NOT NULL,
      url VARCHAR(1024),
      title VARCHAR(1024),
      author VARCHAR(1024),
      etag TEXT,
      lastmodified TEXT
    );

    CREATE TABLE IF NOT EXISTS item (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      feed_id INTEGER NOT NULL,
      guid VARCHAR(64),
      title VARCHAR(1024),
      author VARCHAR(1024),
      url VARCHAR(1024),
      feedurl VARCHAR(1024),
      pubDate TEXT,
      summary TEXT,
      content TEXT,
      FOREIGN KEY(feed_id) REFERENCES feed(id),
      UNIQUE (guid, feed_id)
    );

    CREATE TABLE IF NOT EXISTS subscription (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      destination varchar(100),
      feed_id INTEGER,
      last INTEGER DEFAULT 0,
      created_at timestamp DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY(feed_id) REFERENCES feed(id)
    );
    ]]

  if code != sql.OK
    ivar2\Log 'error', "Error during opendb: #{db\errmsg!}"
    return nil

  return db

prepare = (db, stmt, ...) ->
  code = db\prepare stmt
  unless code
    ivar2\Log 'error', "Error with statement prepare: #{db\errmsg!}"
    return nil
  code\bind_values ...
  return code

-- Function that add functionality to extract specific information for specific sites
feedSpecific = (feedurl, link, title, summary, content) ->
  if feedurl\match 'https://comics.io/my/'

    title = summary\match '
(.-)
' or ''
    img, alt = summary\match '
'
    return "#{html2unicode title} #{img} #{html2unicode alt}"

  return "#{title} - #{link}"

announce = (id) ->
  db = get_db!
  stmt = prepare db, "SELECT DISTINCT feed.id, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id WHERE feed.id = ?", id
  for row in stmt\rows!
    destination = row[2]

    highest = 0
    -- Check last value
    stmt = prepare db, "SELECT last FROM subscription WHERE feed_id = ? and destination = ?", id, destination
    stmt\step!
    value = stmt\get_values()[1]
    if value and value ~= '' and tonumber(value) and tonumber(value) > 0
      highest = value

    ivar2\Log 'debug', 'Got highest %s, %s, %s', id, destination, highest

    stmt = prepare db, "SELECT feed.id AS feed_id, item.id AS item_id, rssurl, pubDate, name, etag, item.url AS link, item.title, content, summary FROM feed JOIN item on item.feed_id = feed.id WHERE feed.id = ? AND item.id > ? ORDER BY item.id DESC LIMIT 100", id, highest
    out = {}
    for item in stmt\nrows!
      with item
        if .item_id > highest
          highest = .item_id
        table.insert out, "[#{bold .name}]: #{feedSpecific(.rssurl, .link, .title, .summary, .content)}"
    if #out > 0
      ivar2\Msg 'privmsg', destination, ivar2.nick, "RSS "..table.concat(out, ', ')
    -- Update last red
    ins = prepare db, 'UPDATE subscription SET last=? WHERE feed_id = ? and destination = ?', highest, id, destination
    code, err = ins\step!
    code, err = ins\finalize!
  db\close!

poll = ->
  db = get_db!
  channels = {}
  for c,_ in pairs(ivar2.channels)
    channels[c] = true
  for row in db\rows "SELECT DISTINCT feed.id, rssurl, lastmodified, name, etag, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id"
    -- Ignore feeds this particular instance of the bot does not subscribe to
    if not channels[row[6]]
      continue
    id = row[1]
    rssurl = row[2]
    lm = row[3]
    name = row[4]
    etag = row[5]
    headers = {}
    if lm and lm != '' then
      headers['If-Modified-Since'] = lm
    if etag and etag != '' then
      headers['If-None-Match'] = etag

    simplehttp {url:rssurl,headers:headers}, (data, url, response) ->
      -- Reopen DB because of async
      sdb = get_db!

      lastModified = response.headers['Last-Modified']
      if not lastModified
        lastModified = response.headers['Date']

      -- Unmodified content
      if response.status_code == 304 or not data
        -- return from the simplehttp callback
        return

      ok, feed, err = pcall -> feedparser.parse data
      if not ok
        ivar2\Log 'error', "#{moduleName}: Error during parsing: <#{feed}> data for feed: <#{name}> with URL <#{url}>"
        return
      if err
        ivar2\Log 'error', "#{moduleName}: Error during parsing: <#{err}> data for feed: <#{name}> with URL <#{url}>"
        return
      else
        title = feed.title
        author = feed.author
        url = feed.link

        -- Update feed with new values
        ins = sdb\prepare 'UPDATE feed SET title=?, author=?, url=?, lastmodified=? WHERE id=?'
        code, err = ins\bind_values title, author, url, lastModified, id
        code, err = ins\step!
        code, err = ins\finalize!

        out = {}
        for i, e in ipairs(feed.entries)
          -- Attempt to get a unique entry ID
          guid = e.guid
          if not guid then guid = e.id
          if not guid then guid = e.link
          if not guid
            ivar2\Log 'error', "#{moduleName}: No GUID when parsing entry: <#{e}> data for feed: <#{name}> with URL <#{url}>"
            break

          ins = sdb\prepare [[
            INSERT OR ABORT INTO
              item(feed_id, guid, title, author, url, pubDate, content, summary)
              VALUES (?, ?, ?, ?, ?, ?, ?, ?)
          ]]
          if not ins then
            ivar2\Log 'error', "#{moduleName}: Error during inserting entry: <#{err}> feed: <#{name}> with URL <#{url}> and position: #{i}"
          else
            ivar2\Log 'debug', "#{moduleName}: Inserting entry: #{e.link}, #{e.title}"
            code = ins\bind_values id, guid, e.title, e.author, e.link, e.updated, e.content, e.summary
            code = ins\step!
            code = ins\finalize!
            if code == sql.CONSTRAINT -- duplicate value
              ivar2\Log 'debug', "Reached duplicate link, breaking"
              break
      sdb\close!
      announce(id)
  db\close!

subscribe = (source, destination, name, url) =>
  db = get_db!
  ins = db\prepare "INSERT INTO feed (name, rssurl) VALUES(?, ?)"
  code = ins\bind_values name, url
  code = ins\step!
  code = ins\finalize!
  ins = db\prepare "INSERT INTO subscription (feed_id, destination) VALUES(?, ?)"
  code = ins\bind_values db\last_insert_rowid!, destination
  code = ins\step!
  code = ins\finalize!
  db\close!
  reply "Ok. Subscribed to #{bold name}"
  poll!

unsubscribe = (source, destination, name) =>
  db = get_db!
  ins = db\prepare "DELETE FROM subscription WHERE feed_id IN (select id from feed where name = ?) and destination = ?"
  code = ins\bind_values name, destination
  code = ins\step!
  code = ins\finalize!
  total_changes = db\total_changes!
  db\close!
  if total_changes < 1
    reply "Wasn't subscribed. But, sure."
  else
    reply "Ok. Stopped caring about #{bold name}"

list = (source, destination) =>
  out = {}
  db = get_db!
  stmt = db\prepare("SELECT name FROM feed JOIN subscription ON feed.id = subscription.feed_id WHERE destination = ?")
  stmt\bind_values destination
  for row in stmt\rows!
    out[#out+1] = row[1]
  if #out > 0 then
    reply "Subscribed to: #{table.concat(out, ', ')}"
  else
    reply "Not subscribed to any feeds."

  db\close!

-- Start the subscribe poller
interval = 60*60
ivar2\Timer(moduleName, interval, interval, poll)

PRIVMSG:
  '^%prss latest (.*)': getLatest
  '^%prss subscribe (.-) (.*)': subscribe
  '^%prss unsubscribe (.*)': unsubscribe
  '^%prss list': list
  -- for debugging
  '^%prss poll': poll

-- Module for subscribing to RSS and announce to channels
feedparser = require 'feedparser' -- http://feedparser.luaforge.net/
{:simplehttp, :json, :urlEncode, :bold} = require'util'
html2unicode = require 'html'
sql = require'lsqlite3'

moduleName = 'rss'

get_db = ->
  db = sql.open 'cache/rss.sql'

  code = db\exec [[
    CREATE TABLE IF NOT EXISTS feed (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT,
      rssurl VARCHAR(1024) NOT NULL,
      url VARCHAR(1024),
      title VARCHAR(1024),
      author VARCHAR(1024),
      etag TEXT,
      lastmodified TEXT
    );

    CREATE TABLE IF NOT EXISTS item (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      feed_id INTEGER NOT NULL,
      guid VARCHAR(64),
      title VARCHAR(1024),
      author VARCHAR(1024),
      url VARCHAR(1024),
      feedurl VARCHAR(1024),
      pubDate TEXT,
      summary TEXT,
      content TEXT,
      FOREIGN KEY(feed_id) REFERENCES feed(id),
      UNIQUE (guid, feed_id)
    );

    CREATE TABLE IF NOT EXISTS subscription (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      destination varchar(100),
      feed_id INTEGER,
      last INTEGER DEFAULT 0,
      created_at timestamp DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY(feed_id) REFERENCES feed(id)
    );
    ]]

  if code != sql.OK
    ivar2\Log 'error', "Error during opendb: #{db\errmsg!}"
    return nil

  return db

prepare = (db, stmt, ...) ->
  code = db\prepare stmt
  unless code
    ivar2\Log 'error', "Error with statement prepare: #{db\errmsg!}"
    return nil
  code\bind_values ...
  return code

-- Function that add functionality to extract specific information for specific sites
feedSpecific = (feedurl, link, title, summary, content) ->
  if feedurl\match 'https://comics.io/my/'

    title = summary\match '
(.-)
' or ''
    img, alt = summary\match '
'
    return "#{html2unicode title} #{img} #{html2unicode alt}"

  return "#{title} - #{link}"

announce = (id) ->
  db = get_db!
  stmt = prepare db, "SELECT DISTINCT feed.id, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id WHERE feed.id = ?", id
  for row in stmt\rows!
    destination = row[2]

    highest = 0
    -- Check last value
    stmt = prepare db, "SELECT last FROM subscription WHERE feed_id = ? and destination = ?", id, destination
    stmt\step!
    value = stmt\get_values()[1]
    if value and value ~= '' and tonumber(value) and tonumber(value) > 0
      highest = value

    ivar2\Log 'debug', 'Got highest %s, %s, %s', id, destination, highest

    stmt = prepare db, "SELECT feed.id AS feed_id, item.id AS item_id, rssurl, pubDate, name, etag, item.url AS link, item.title, content, summary FROM feed JOIN item on item.feed_id = feed.id WHERE feed.id = ? AND item.id > ? ORDER BY item.id DESC LIMIT 100", id, highest
    out = {}
    for item in stmt\nrows!
      with item
        if .item_id > highest
          highest = .item_id
        table.insert out, "[#{bold .name}]: #{feedSpecific(.rssurl, .link, .title, .summary, .content)}"
    if #out > 0
      ivar2\Msg 'privmsg', destination, ivar2.nick, "RSS "..table.concat(out, ', ')
    -- Update last red
    ins = prepare db, 'UPDATE subscription SET last=? WHERE feed_id = ? and destination = ?', highest, id, destination
    code, err = ins\step!
    code, err = ins\finalize!
  db\close!

poll = ->
  db = get_db!
  channels = {}
  for c,_ in pairs(ivar2.channels)
    channels[c] = true
  for row in db\rows "SELECT DISTINCT feed.id, rssurl, lastmodified, name, etag, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id"
  -- Ignore feeds this particular instance of the bot does not subscribe to
    if not channels[row[6]]
      continue
    id = row[1]
    rssurl = row[2]
    lm = row[3]
    name = row[4]
    etag = row[5]
    headers = {}
    if lm and lm != '' then
      headers['If-Modified-Since'] = lm
    if etag and etag != '' then
      headers['If-None-Match'] = etag

    simplehttp {url:rssurl,headers:headers}, (data, url, response) ->
      -- Reopen DB because of async
      sdb = get_db!

      lastModified = response.headers['Last-Modified']
      if not lastModified
        lastModified = response.headers['Date']

      -- Unmodified content
      if response.status_code == 304 or not data
        -- return from the simplehttp callback
        return

      ok, feed, err = pcall -> feedparser.parse data
      if not ok
        ivar2\Log 'error', "#{moduleName}: Error during parsing: <#{feed}> data for feed: <#{name}> with URL <#{url}>"
        return
      if err
        ivar2\Log 'error', "#{moduleName}: Error during parsing: <#{err}> data for feed: <#{name}> with URL <#{url}>"
        return
      else
        title = feed.title
        author = feed.author
        url = feed.link

        -- Update feed with new values
        ins = sdb\prepare 'UPDATE feed SET title=?, author=?, url=?, lastmodified=? WHERE id=?'
        code, err = ins\bind_values title, author, url, lastModified, id
        code, err = ins\step!
        code, err = ins\finalize!

        out = {}
        for i, e in ipairs(feed.entries)
          -- Attempt to get a unique entry ID
          guid = e.guid
          if not guid then guid = e.id
          if not guid then guid = e.link
          if not guid
            ivar2\Log 'error', "#{moduleName}: No GUID when parsing entry: <#{e}> data for feed: <#{name}> with URL <#{url}>"
            break

          ins = sdb\prepare [[
            INSERT OR ABORT INTO
              item(feed_id, guid, title, author, url, pubDate, content, summary)
              VALUES (?, ?, ?, ?, ?, ?, ?, ?)
          ]]
          if not ins then
            ivar2\Log 'error', "#{moduleName}: Error during inserting entry: <#{err}> feed: <#{name}> with URL <#{url}> and position: #{i}"
          else
            ivar2\Log 'info', "#{moduleName}: Inserting entry: #{e.link}, #{e.title}"
            code = ins\bind_values id, guid, e.title, e.author, e.link, e.updated, e.content, e.summary
            code = ins\step!
            code = ins\finalize!
            if code == sql.CONSTRAINT -- duplicate value
              ivar2\Log 'info', "Reached duplicate link, breaking"
              break
      sdb\close!
      announce(id)
  db\close!

subscribe = (source, destination, name, url) =>
  db = get_db!
  ins = db\prepare "INSERT INTO feed (name, rssurl) VALUES(?, ?)"
  code = ins\bind_values name, url
  code = ins\step!
  code = ins\finalize!
  ins = db\prepare "INSERT INTO subscription (feed_id, destination) VALUES(?, ?)"
  code = ins\bind_values db\last_insert_rowid!, destination
  code = ins\step!
  code = ins\finalize!
  db\close!
  reply "Ok. Subscribed to #{bold name}"
  poll!

unsubscribe = (source, destination, name) =>
  db = get_db!
  ins = db\prepare "DELETE FROM subscription WHERE feed_id IN (select id from feed where name = ?) and destination = ?"
  code = ins\bind_values name, destination
  code = ins\step!
  code = ins\finalize!
  total_changes = db\total_changes!
  db\close!
  if total_changes < 1
    reply "Wasn't subscribed. But, sure."
  else
    reply "Ok. Stopped caring about #{bold name}"

list = (source, destination) =>
  out = {}
  db = get_db!
  stmt = db\prepare("SELECT name FROM feed JOIN subscription ON feed.id = subscription.feed_id WHERE destination = ?")
  stmt\bind_values destination
  for row in stmt\rows!
    out[#out+1] = row[1]
  if #out > 0 then
    reply "Subscribed to: #{table.concat(out, ', ')}"
  else
    reply "Not subscribed to any feeds."

  db\close!

-- Start the subscribe poller
ivar2\Timer(moduleName, 60*5, 60*5, poll)

PRIVMSG:
  '^%prss latest (.*)': getLatest
  '^%prss subscribe (.-) (.*)': subscribe
  '^%prss unsubscribe (.*)': unsubscribe
  '^%prss list': list
  -- for debugging
  '^%prss poll': poll


Properly handle deletion of shaders and programs.

- Also fixed some syntax errors.

-- 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

Promise = require "glurg.common.Promise"
Filter = require "glurg.filters.Filter"
Call = require "glurg.trace.Call"
Trace = require "glurg.trace.Trace"

class Shader
	new: (name, shader_type) =>
		@name = name
		@type = shader_type
		@source = ""
		@is_deleted = false

	_extract_source: (call) =>
		Promise.keep("@name == shader",
			@name == call\get_argument_by_name("shader")\query!)

		s = {}
		argument = call\get_argument_by_name("string")
		for i = 1, #argument
			table.insert(s, argument[i]\query!)
		@source = table.concat(s, "\n")

class Program
	new: (name) =>
		@name = name
		@attributes = {}
		@shaders = {}
		@is_deleted = false

	_set_attribute: (name, index) =>
		Promise.keep("name", Promise.IsString(name))
		Promise.keep("index", Promise.IsNumber(index))

		@attributes[name] = index
		@attributes[index] = name

	get_attribute: (key) =>
		return switch type(key)
			when 'number', 'string' then @attributes[key]
			else error "invalid key type, expected string or number"

	has_attribute: (key) =>
		return switch type(key)
			when 'number', 'string' then @attributes[key] != nil
			else error "invalid key type, expected string or number"

	_attach_shader: (shader) =>
		Promise.keep("shader", Promise.IsClass(shader, Shader))

		if @shaders[shader.type] == nil
			@shaders[shader.type] = {}

		table.insert(@shaders[shader.type], shader)

	_detach_shader: (shader) =>
		Promise.keep("shader", Promise.IsClass(shader, Shader))

		if @shaders[shader.type]
			for i = 1, #@shaders[shader.type]
				if @shaders[shader.type][i] == shader then
					table.remove(@shaders[shader.type], i)
					break

	get_shader_source: (shader_type) =>
		source = [s for i, s in ipairs(@shaders[shader_type] or {})]
		return table.concat(source, "\n")

class ProgramFilter extends Filter
	new: =>
		@_shaders = {}
		@_programs = {}
		@current_program = 0

	_get_shader: (name) =>
		Promise.keep("@_shaders[name] != nil", @_shaders[name] != nil)

		return @_shaders[name]

	_add_shader: (shader) =>
		Promise.keep("@_shaders[shader.name] == nil",
			@_shaders[shader.name] == nil)

		@_shaders[shader.name] = shader

	_remove_shader: (shader) =>
		@_shaders[shader] = nil

	_get_program: (name) =>
		Promise.keep("@_programs[name] != nil", @_programs[name] != nil)

		return @_programs[name]

	_add_program: (program) =>
		Promise.keep("@_programs[program.name] == nil",
			@_programs[program.name] == nil)

		@_programs[program.name] = program

	_remove_program: (program) =>
		@_programs[program] = nil

	get_current_program: =>
		return @\_get_program(@current_program)

	glCreateShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		shader_type = call\get_argument_by_name("type")\query!.value_name
		name = call.result\query!

		@\_add_shader(Shader(name, shader_type))

		return false

	glShaderSource: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		name = call\get_argument_by_name("shader")\query!
		@\_get_shader(name)\_extract_source(call)

		return false

	glCreateProgram: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		name = call.result\query!
		@\_add_program(Program(name))

		return false

	glAttachShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		shader = call\get_argument_by_name("shader")\query!

		@\_get_program(program)\_attach_shader(@\_get_shader(shader))

		return false

	glDetachShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		shader = call\get_argument_by_name("shader")\query!

		@\_get_program(program)\_detach_shader(@\_get_shader(shader))

		return false

	glDeleteShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		shader = call\get_argument_by_name("shader")\query!
		@\_get_shader(shader).is_deleted = true
		@\_remove_shader(shader)

		return false

	glDeleteProgram: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		@\_get_program(program).is_deleted = true
		@\_remove_program(program)

		return false

	glBindAttribLocation: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		name = call\get_argument_by_name("name")\query!
		index = call\get_argument_by_name("index")\query!

		@\_get_program(program)\_set_attribute(name, index)

		return false

return ProgramFilter

-- 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

Promise = require "glurg.common.Promise"
Filter = require "glurg.filters.Filter"
Call = require "glurg.trace.Call"
Trace = require "glurg.trace.Trace"

class Shader
	new: (name, shader_type) =>
		@name = name
		@type = shader_type
		@source = ""
		@is_deleted = false

	_extract_source: (call) =>
		Promise.keep("@name == shader",
			@name == call\get_argument_by_name("shader")\query!)

		s = {}
		argument = call\get_argument_by_name("string")
		for i = 1, #argument
			table.insert(s, argument[i]\query!)
		@source = table.concat(s, "\n")

class Program
	new: (name) =>
		@name = name
		@attributes = {}
		@shaders = {}
		@is_deleted = false

	_set_attribute: (name, index) =>
		Promise.keep("name", Promise.IsString(name))
		Promise.keep("index", Promise.IsNumber(index))

		@attributes[name] = index
		@attributes[index] = name

	get_attribute: (key) =>
		return switch type(key)
			when 'number', 'string' then @attributes[key]
			else error "invalid key type, expected string or number"

	has_attribute: (key) =>
		return switch type(key)
			when 'number', 'string' then @attributes[key] != nil
			else error "invalid key type, expected string or number"

	_attach_shader: (shader) =>
		Promise.keep("shader", Promise.IsClass(shader, Shader))

		if @shaders[shader.type] == nil
			@shaders[shader.type] = {}

		table.insert(@shaders[shader.type], shader)

	_detach_shader: (shader) =>
		Promise.keep("shader", Promise.IsClass(shader, Shader))

		if @shaders[shader.type]
			for i = 1, #@shaders[shader.type]
				if @shaders[shader.type][i] == shader then
					table.remove(@shaders[shader.type], i)
					break

	get_shader_source: (shader_type) =>
		source = [s for i, s in ipairs(@shaders[shader_type] or {})]
		return table.concat(source, "\n")

class ProgramFilter extends Filter
	new: =>
		@_shaders = {}
		@_programs = {}
		@current_program = 0

	_get_shader: (name) =>
		Promise.keep("@_shaders[name] != nil", @_shaders[name] != nil)

		return @_shaders[name]

	_add_shader: (shader) =>
		Promise.keep("@_shaders[shader.name] == nil",
			@_shaders[shader.name] == nil)

		@_shaders[shader.name] = shader

	_get_program: (name) =>
		Promise.keep("@_programs[name] != nil", @_programs[name] != nil)

		return @_programs[name]

	_add_program: (program) =>
		Promise.keep("@_programs[program.name] == nil",
			@_programs[program.name] == nil)

		@_programs[program.name] = program

	get_current_program: =>
		return @\_get_program(@current_program)

	glCreateShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		shader_type = call\get_argument_by_name("type")\query!.value_name
		name = call.result\query!

		@\_add_shader(Shader(name, shader_type))

		return false

	glShaderSource: (trace, call)
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		name = call\get_argument_by_name("shader")\query!
		@\_get_shader(name)\_extract_source(call)

		return false

	glCreateProgram: (trace, call)
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		name = call.result\query!
		@\_add_program(Program(name))

		return false

	glAttachShader: (trace, call)
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		shader = call\get_argument_by_name("shader")\query!

		@\_get_program(program)\_attach_shader(@\_get_shader(shader))

		return false

	glDetachShader: (trace, call)
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		shader = call\get_argument_by_name("shader")\query!

		@\_get_program(program)\_detach_shader(@\_get_shader(shader))

		return false

	glDeleteShader: (trace, call)
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		shader = call\get_argument_by_name("shader")\query!
		@\_get_shader(shader).is_deleted = true

		return false

	glDeleteProgram: (trace, call)
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		@\_get_program(program).is_deleted = true

		return false

	glBindAttribLocation: (trace, call)
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		name = call\get_argument_by_name("name")\query!
		index = call\get_argument_by_name("index")\query!

		@\_get_program(program)\_set_attribute(name, index)

		return false

return ProgramFilter