examples/ibb.lua
author Myhailo Danylenko <isbear@ukrpost.net>
Sat, 21 Mar 2009 03:42:49 +0200
changeset 41 9e39fd8a20df
child 43 7c22b1f2c6e5
permissions -rw-r--r--
Updated to library module ibb


-- IN-BAND BYTESTREAMS (XEP-0047)

-- library

require 'lm'
require 'base64'

iq = { }

function iq.send ( conn, to, smtype, data, success, fail )
	data.mtype = 'iq-' .. smtype
	data.to    = to
	conn:send ( lm.message.create ( data ),
		function ( conn, mess )
			local mtype, smtype = mess:type ()
			if smtype == 'result' then
				success ()
			elseif smtype == 'error' then
				fail ( mess:child( 'error' ):children():name () ) -- FIXME
			else
				fail ( mess:xml () )
				return false
			end
			return true
		end )
end

-- public

ibb = {
	block_size    = 4096,
	streamhandler =
		function ( accept, reject )
			reject ()
		end,
}

local ibb_sid = 0 -- private

function ibb.send ( conn, to, success, fail, id )
	local bs    = ibb.block_size -- local instance
	local sid   = id
	if not sid then
		ibb_sid = ibb_sid + 1
		sid     = 'ibb_' .. ibb_sid
	end
	iq.send ( conn, to, 'set',
		{
			open = { sid = sid, ['block-size'] = bs, xmlns = 'http://jabber.org/protocol/ibb' }
		},
		function ()
			local seq   = 0
			local noerr = true
			success (
				function ( data, success, fail )
					if not data  then
						iq.send ( conn, to, 'set',
							{
								close = { sid = sid, xmlns = 'http://jabber.org/protocol/ibb' },
							},
							function ()
								success ( 'end' ) -- XXX
							end,
							fail )
					elseif data ~= '' then
						local encoded = base64.encode ( data )
						while encoded:len () > 0 and noerr do
							local chunk = encoded:sub ( 1, bs )
							local cseq  = seq -- local instance for closure
							encoded     = encoded:sub ( bs + 1 )
							seq         = seq + 1
							iq.send ( conn, to, 'set',
								{
									data = { sid = sid, xmlns = 'http://jabber.org/protocol/ibb', seq = cseq,
										chunk,
									},
								},
								function ()
									success ( cseq )
								end,
								function ( mesg )
									noerr = false
									fail ( mesg )
								end )
						end
					end
				end )
		end,
		fail )
end

-- private

local ibb_files               = {}
local ibb_handler_registered  = false
local ibb_incoming_iq_handler = lm.message_handler.new ( 
	function ( conn, mess )
		local mtype, smtype = mess:type ()
		if smtype ~= 'set' then
			return false
		end

		local child = mess:children ()
		if not child or child:attribute ( 'xmlns' ) ~= 'http://jabber.org/protocol/ibb' then
			return false
		end

		local id     = mess:attribute ( 'id' )
		local from   = mess:attribute ( 'from' )
		local action = child:name ()
		local sid    = child:attribute ( 'sid' )

		if action == 'open' then
			if not ibb_files[sid] then
				ibb.streamhandler ( from,
					function ( success, fail )
						ibb_files[sid] = { from = from, success = success, fail = fail }
						conn:send ( lm.message.create { to = from, mtype = 'iq-result', id = id } )
					end,
					function ()
						conn:send (
							lm.message.create { to = from, mtype = 'iq-error', id = id,
								error = { code = '405', type = 'cancel',
									['not-allowed'] = { xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas' }
								}
							} )
					end )
			else
				conn:send (
					lm.message.create { to = from, mtype = 'iq-error', id = id,
						error = { code = '409', type = 'cancel',
							conflict = { xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas' }
						}
					} )
			end
		elseif action == 'data' then
			local seq = child:attribute ( 'seq' )
			if ibb_files[sid] and from == ibb_files[sid].from and not ibb_files[sid][tonumber(seq)+1] then
				local data = child:value ()
				conn:send ( lm.message.create { to = from, mtype = 'iq-result', id = id } )
				ibb_files[sid][tonumber(seq)+1] = data
-- XXX			ibb_files[sid].success ( seq )
			else
				if ibb_files[sid] then
					ibb_files[sid].fail ( 'conflict' )
					ibb_files[sid] = nil -- invalidate session
					conn:send (
						lm.message.create { to = from, mtype = 'iq-error', id = id,
							error = { code = '409', type = 'cancel',
								conflict = { xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas' }
							}
						} )
				else
					conn:send (
						lm.message.create { to = from, mtype = 'iq-error', id = id,
							error = { code = '404', type = 'cancel', -- XXX: check
								['item-not-found'] = { xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas' }
							}
						} )
				end
			end
		elseif action == 'close' then
			if ibb_files[sid] and from == ibb_files[sid].from then
				local data = ''
				for seq, chunk in ipairs ( ibb_files[sid] ) do
					data = data .. chunk
				end
				local decoded = base64.decode ( data )
				ibb_files[sid].success ( decoded )
				conn:send ( lm.message.create { to = from, mtype = 'iq-result', id = id } )
				ibb_files[sid] = nil
			else
				if ibb_files[sid] then
					ibb_files[sid].fail ( 'conflict' )
					ibb_files[sid] = nil -- invalidate session
					conn:send (
						lm.message.create { to = from, mtype = 'iq-error', id = id,
							error = { code = '409', type = 'cancel',
								conflict = { xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas' }
							}
						} )
				else
					conn:send (
						lm.message.create { to = from, mtype = 'iq-error', id = id,
							error = { code = '404', type = 'cancel', -- XXX: check
								['item-not-found'] = { xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas' }
							}
						} )
				end
			end
		else
			return false
		end

		return true
	end )

-- mcabber

local mc_incoming_files = { }

ibb.streamhandler =
	function ( from, accept, reject )
		local fid = #mc_incoming_files + 1
		mc_incoming_files[fid] = {
				from   = from,
				accept =
					function ( name )
						mc_incoming_files[fid].name = name
						accept (
							function ( data )
								local h = io.open ( mc_incoming_files[fid].name, 'w' )
								if not h then
									print ( 'Cannot open output file: ' .. mc_incoming_files[fid].name )
									return
								end
								h:write ( data )
								h:close ()
								print ( 'Stream ' .. fid .. ' successfully saved to ' .. mc_incoming_files[fid].name )
								mc_incoming_files[fid] = nil
							end,
							function ( mesg )
								main.print_info ( from, 'Stream error: ' .. mesg )
								mc_incoming_files[fid] = nil -- XXX
							end )
					end,
				reject =
					function ()
						reject ()
						print ( 'Stream ' .. fid .. ' rejected' )
						mc_incoming_files[fid] = nil
					end,
		}
		main.print_info ( from, from .. ' wants you to receive stream. Use /ibb [accept|reject] ' .. fid .. ' to process his request.' )
	end

main.command ( 'ibb',
	function ( args )
		local action = args[1]
		if action == 'send' then
			local who
			if args.t then
				who = args.t
			else
				who = main.full_jid ()
			end
			local fname = args[2]
			ibb.send ( lm.connection.bless ( main.connection () ), who,
				function ( sender )
					main.print_info ( who, 'Stream accepted' )
					local noerr = true
					local h     = io.open ( fname, 'r' )
					if not h then
						print ( 'Cannot open file ' .. fname )
						return
					end
					local data = h:read ( '*a' ) -- In fact, it is better to read it in chunks :/
					h:close ()
					local fail =
						function ( mesg )
							noerr = false
							main.print_info ( who, 'Stream error: ' .. mesg )
						end
					sender ( data,
						function ( seq )
							main.print_info ( who, 'Delivery notification of chunk #' .. seq )
						end, fail )
					if noerr then
						sender ( nil,
							function ( seq )
								main.print_info ( who, 'Stream finalizing notification' )
							end, fail )
					end
					if noerr then
						main.print_info ( who, 'Stream sent' )
					else
						main.print_info ( who, 'Stream error occured' )
					end
				end,
				function ( mesg )
					main.print_info ( who, 'Stream initiation error: ' .. mesg )
				end )
		elseif action == 'accept' then
			local id = tonumber(args[2])
			if mc_incoming_files[id] then
				mc_incoming_files[id].accept ( args[3] )
			end
		elseif action == 'reject' then
			local id = tonumber(args[2])
			if mc_incoming_files[id] then
				mc_incoming_files[id].reject ()
			end
		else
			local text = ''
			for sid, data in pairs ( mc_incoming_files ) do
				text = text .. '\n' ..  sid .. ': ' .. data.from .. ' --> ' .. ( data.name or '?' )
			end
			if text ~= '' then
				print ( 'List of incoming streams:' .. text )
			else
				print ( 'No streams' )
			end
		end
	end, true, { "send", "accept", "reject" } )


commands_help['ibb'] = "[[-t target_jid] send filename | accept sid filename | reject sid]\n\nRequests, accepts or rejects sending file via in-band bytestream."

hooks_d['hook-post-connect'].ibb =
	function ( args )
		lm.connection.bless( main.connection () ):handler ( ibb_incoming_iq_handler, 'iq', 'normal' )
		ibb_handler_registered = true
		hooks_d['hook-post-connect'].ibb = nil
		hooks_d['hook-quit'].ibb =
			function ( args )
				if ibb_handler_registered then
					lm.connection.bless( main.connection () ):handler ( ibb_incoming_iq_handler, 'iq' )
				end
			end
	end

main.add_feature ( 'http://jabber.org/protocol/ibb' )

-- vim: se ts=4: --