examples/xep0047.lua
author Myhailo Danylenko <isbear@ukrpost.net>
Thu, 19 Mar 2009 10:16:20 +0200
changeset 38 1f141d9a081a
parent 34 8206d7cb1447
child 42 18d801679feb
permissions -rw-r--r--
IBB uses user connection, optimization


receiving_files = {}
ibb_block_size  = 4096
current_sid_number = 0

-- FIXME: read from /dev/urandom?
function gen_unique_sid ()
	current_sid_number = current_sid_number + 1
	return 'mc-' .. tostring ( current_sid_number )
end

ibb_incoming_iq_handler = lm.message_handler.new ( 
	function ( conn, mess )
		local id   = mess:attribute ( 'id' )
		local from = mess:attribute ( 'from' )

		local open = mess:child ( 'open' )
		if open and open:attribute ( 'xmlns' ) == 'http://jabber.org/protocol/ibb' then
			local sid  = open:attribute ( 'sid' )
			if not receiving_files[sid] then
				local buffer = ''
				receiving_files[sid] = { from = from, status = 'pending' }
				receiving_files[sid].accept =
					function ( name )
						main.print_info ( from, string.format ( "Receiving stream from %s, id %s", from, sid ) )
						conn:send ( lm.message.create { to = from, mtype = 'iq-result', id = id } )
						receiving_files[sid].name = name
						receiving_files[sid].status = 'accepted'
					end
				receiving_files[sid].reject =
					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' }
								}
							} )
						receiving_files[sid].status = 'rejected'
					end
				print ( 'You have a new bytestream to receive. To save it use /ibb accept ' .. sid .. ' filename' )
			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
			return true
		end

		local qdata = mess:child ( 'data' )
		if qdata and qdata:attribute ( 'xmlns' ) == 'http://jabber.org/protocol/ibb' then
			local sid  = qdata:attribute ( 'sid' )
			local seq  = qdata:attribute ( 'seq' )
			if receiving_files[sid] and from == receiving_files[sid].from and not receiving_files[sid][tonumber(seq)+1] then
				local data = qdata:value ()
				main.print_info ( from, string.format ( " - stream part %s, id %s, %d bytes", seq, sid, data:len() ) )
				conn:send ( lm.message.create { to = from, mtype = 'iq-result', id = id } )
				receiving_files[sid][tonumber(seq)+1] = data
			else
				receiving_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' }
						}
					} )
			end
			return true
		end

		local close = mess:child ( 'close' )
		if close and close:attribute ( 'xmlns' ) == 'http://jabber.org/protocol/ibb' then
			local sid  = close:attribute ( 'sid' )
			if receiving_files[sid] and from == receiving_files[sid].from then
				main.print_info ( from, "Done with stream id " .. sid )
				conn:send ( lm.message.create { to = from, mtype = 'iq-result', id = id } )
				local decoder = io.popen ( string.format ( "base64 -d -i >%q", receiving_files[sid].name ), "w" )
				if not decoder then
					main.print_info ( from, "Error opening decoder" )
				else
					for i, v in ipairs ( receiving_files[sid] ) do
						decoder:write ( v )
					end
					decoder:close ()
				end
			else
				receiving_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' }
						}
					} )
			end
			return true
		end

		return false
	end )

function ibb_send_file ( conn, to, name )
	if not to then
		to = main.full_jid ()
	elseif not to:match ( "/" ) then
		to = main.full_jid ( to )
	end
	local sid = gen_unique_sid ()
	conn:send (
		lm.message.create { to = to, mtype = 'iq-set',
			open = { sid = sid, ['block-size'] = ibb_block_size, xmlns = 'http://jabber.org/protocol/ibb' }
		},
		function ( conn, message )
			if message:child ( 'error' ) then
				main.print_info ( to, "Stream request refused: " .. message:child( 'error' ):children():name () )
			else
				main.print_info ( to, "Stream accepted, starting sequence" )
				local buffer = ''
				main.bgread ( string.format ( 'base64 -w 0 %q', name ),
					function ( data )
						if data then
							buffer = buffer .. data
							return true
						else
							local seq = 0
							local msgbuf = buffer:sub ( 1, ibb_block_size )
							buffer = buffer:sub ( ibb_block_size + 1 )
							local function handler ( conn, message )
								if message:child ( 'error' ) then
									main.print_info ( to, "Stream error, transfer ceased at seq = " .. seq .. ": " .. message:child( 'error' ):children():name () )
								else
									main.print_info ( to, " - acquired seq = " .. seq )
									seq = seq + 1
									if buffer:len () == 0 then
										conn:send (
											lm.message.create { to = to, mtype = 'iq-set',
												close = { sid = sid, xmlns = 'http://jabber.org/protocol/ibb' }
											},
											function ( conn, message )
												if message:child ( 'error' ) then
													main.print_info ( to, "Error at closing stream: " .. message:child( 'error' ):children():name () )
												else
													main.print_info ( to, "File successfully transferred" )
												end
												return true
											end )
									else
										local msgbuf = buffer:sub ( 1, ibb_block_size )
										buffer = buffer:sub ( ibb_block_size )
										conn:send (
											lm.message.create { to = to, mtype = 'iq-set',
												data = { sid = sid, xmlns = 'http://jabber.org/protocol/ibb', seq = seq,
													msgbuf
												}
											},
											handler )
									end
								end
								return true
							end
							conn:send (
								lm.message.create { to = to, mtype = 'iq-set',
									data = { sid = sid, xmlns = 'http://jabber.org/protocol/ibb', seq = seq,
										msgbuf
									}
								},
								handler )
							return false
						end
					end )
			end
			return true
		end )
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
			ibb_send_file ( lm.connection.bless ( main.connection () ), who, args[2] )
		elseif action == 'accept' then
			local id = args[2]
			if receiving_files[id] then
				receiving_files[id].accept ( args[3] )
			end
		elseif action == 'reject' then
			local id = args[2]
			if receiving_files[id] then
				receiving_files[id].reject ()
			end
		elseif action == 'del' then
			local id = args[2]
			receiving_files[id] = nil
		else
			local text = ''
			for sid, data in pairs ( receiving_files ) do
				text = text .. '\n' ..  sid .. ': ' .. ( data.name or '(not set)' ) .. ' [' .. data.status .. ']'
			end
			if text ~= '' then
				print ( 'List of incoming streams:' .. text )
			else
				print ( 'No streams' )
			end
		end
	end, true, { "send", "accept", "reject", "del" } )

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

hooks_d['hook-post-connect'].xep0047 =
	function ( args )
		lm.connection.bless( main.connection () ):handler ( ibb_incoming_iq_handler, 'iq', 'normal' )
		ibb_handler_registered = true
		hooks_d['hook-post-connect'].xep0047 = nil
		hooks_d['hook-quit'].xep0047 =
			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: --