Mercurial > prosody-modules
comparison mod_ircd/mod_ircd.lua @ 326:282abba844e8
mod_ircd: Partial rewrite, use verse for MUC
| author | Kim Alvefur <zash@zash.se> |
|---|---|
| date | Wed, 02 Feb 2011 23:52:31 +0100 |
| parents | e5cc0fe5eec3 |
| children | febfb59502fc |
comparison
equal
deleted
inserted
replaced
| 325:4e50e591a7fc | 326:282abba844e8 |
|---|---|
| 1 -- README | |
| 2 -- Squish verse into this dir, then squish them into one, which you move | |
| 3 -- and rename to mod_ircd.lua in your prosody modules/plugins dir. | |
| 4 -- | |
| 5 -- IRC spec: | |
| 6 -- http://tools.ietf.org/html/rfc2812 | |
| 7 local _module = module | |
| 8 module = _G.module | |
| 9 local module = _module | |
| 10 -- | |
| 11 local component_jid, component_secret, muc_server = | |
| 12 module.host, nil, module:get_option("conference_server"); | |
| 13 | |
| 14 package.loaded["util.sha1"] = require "util.encodings"; | |
| 15 local verse = require "verse" | |
| 16 require "verse.component" | |
| 17 require "socket" | |
| 18 c = verse.new();--verse.logger()) | |
| 19 c:add_plugin("groupchat"); | |
| 20 | |
| 21 local function verse2prosody(e) | |
| 22 return c:event("stanza", e.stanza) or true; | |
| 23 end | |
| 24 module:hook("message/bare", verse2prosody); | |
| 25 module:hook("message/full", verse2prosody); | |
| 26 module:hook("presence/bare", verse2prosody); | |
| 27 module:hook("presence/full", verse2prosody); | |
| 28 c.type = "component"; | |
| 29 c.send = core_post_stanza; | |
| 30 | |
| 31 -- This plugin is actually a verse based component, but that mode is currently commented out | |
| 32 | |
| 33 -- Add some hooks for debugging | |
| 34 --c:hook("opened", function () print("Stream opened!") end); | |
| 35 --c:hook("closed", function () print("Stream closed!") end); | |
| 36 --c:hook("stanza", function (stanza) print("Stanza:", stanza) end); | |
| 37 | |
| 38 -- This one prints all received data | |
| 39 --c:hook("incoming-raw", print, 1000); | |
| 40 --c:hook("stanza", print, 1000); | |
| 41 --c:hook("outgoing-raw", print, 1000); | |
| 42 | |
| 43 -- Print a message after authentication | |
| 44 --c:hook("authentication-success", function () print("Logged in!"); end); | |
| 45 --c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); | |
| 46 | |
| 47 -- Print a message and exit when disconnected | |
| 48 --c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); | |
| 49 | |
| 50 -- Now, actually start the connection: | |
| 51 --c.connect_host = "127.0.0.1" | |
| 52 --c:connect_component(component_jid, component_secret); | |
| 53 | |
| 54 local jid = require "util.jid"; | |
| 55 | |
| 56 local function irc2muc(channel, nick) | |
| 57 return jid.join(channel:gsub("^#", ""), muc_server, nick) | |
| 58 end | |
| 59 local function muc2irc(room) | |
| 60 local channel, _, nick = jid.split(room); | |
| 61 return "#"..channel, nick; | |
| 62 end | |
| 63 | |
| 1 local irc_listener = { default_port = 6667, default_mode = "*l" }; | 64 local irc_listener = { default_port = 6667, default_mode = "*l" }; |
| 2 | 65 |
| 3 local sessions = {}; | 66 local sessions = {}; |
| 67 local jids = {}; | |
| 4 local commands = {}; | 68 local commands = {}; |
| 5 | 69 |
| 6 local nicks = {}; | 70 local nicks = {}; |
| 7 | 71 |
| 8 local st = require "util.stanza"; | 72 local st = require "util.stanza"; |
| 9 | 73 |
| 10 local conference_server = module:get_option("conference_server") or "conference.jabber.org"; | 74 local conference_server = muc_server; |
| 11 | 75 |
| 12 local function irc_close_session(session) | 76 local function irc_close_session(session) |
| 13 session.conn:close(); | 77 session.conn:close(); |
| 14 end | 78 end |
| 15 | 79 |
| 16 function irc_listener.onincoming(conn, data) | 80 function irc_listener.onincoming(conn, data) |
| 17 local session = sessions[conn]; | 81 local session = sessions[conn]; |
| 18 if not session then | 82 if not session then |
| 19 session = { conn = conn, host = module.host, reset_stream = function () end, | 83 session = { conn = conn, host = component_jid, reset_stream = function () end, |
| 20 close = irc_close_session, log = logger.init("irc"..(conn.id or "1")), | 84 close = irc_close_session, log = logger.init("irc"..(conn.id or "1")), |
| 85 rooms = {}, | |
| 21 roster = {} }; | 86 roster = {} }; |
| 22 sessions[conn] = session; | 87 sessions[conn] = session; |
| 23 function session.data(data) | 88 function session.data(data) |
| 24 module:log("debug", "Received: %s", data); | |
| 25 local command, args = data:match("^%s*([^ ]+) *(.*)%s*$"); | 89 local command, args = data:match("^%s*([^ ]+) *(.*)%s*$"); |
| 26 if not command then | 90 if not command then |
| 27 module:log("warn", "Invalid command: %s", data); | |
| 28 return; | 91 return; |
| 29 end | 92 end |
| 30 command = command:upper(); | 93 command = command:upper(); |
| 31 module:log("debug", "Received command: %s", command); | |
| 32 if commands[command] then | 94 if commands[command] then |
| 33 local ret = commands[command](session, args); | 95 local ret = commands[command](session, args); |
| 34 if ret then | 96 if ret then |
| 35 session.send(ret.."\r\n"); | 97 session.send(ret.."\r\n"); |
| 36 end | 98 end |
| 99 else | |
| 100 module:log("debug", "Unknown command: %s", command); | |
| 37 end | 101 end |
| 38 end | 102 end |
| 39 function session.send(data) | 103 function session.send(data) |
| 40 module:log("debug", "sending: %s", data); | |
| 41 return conn:write(data.."\r\n"); | 104 return conn:write(data.."\r\n"); |
| 42 end | 105 end |
| 43 end | 106 end |
| 44 if data then | 107 if data then |
| 45 session.data(data); | 108 session.data(data); |
| 46 end | 109 end |
| 47 end | 110 end |
| 48 | 111 |
| 49 function irc_listener.ondisconnect(conn, error) | 112 function irc_listener.ondisconnect(conn, error) |
| 50 module:log("debug", "Client disconnected"); | 113 local session = sessions[conn]; |
| 114 for _, room in pairs(session.rooms) do | |
| 115 room:leave("Disconnected"); | |
| 116 end | |
| 51 sessions[conn] = nil; | 117 sessions[conn] = nil; |
| 52 end | 118 end |
| 53 | 119 |
| 54 function commands.NICK(session, nick) | 120 function commands.NICK(session, nick) |
| 121 if session.nick then | |
| 122 session.send(":"..session.host.." 484 * "..nick.." :I'm afraid I can't let you do that, "..nick); | |
| 123 return; | |
| 124 end | |
| 55 nick = nick:match("^[%w_]+"); | 125 nick = nick:match("^[%w_]+"); |
| 56 if nicks[nick] then | 126 if nicks[nick] then |
| 57 session.send(":"..session.host.." 433 * "..nick.." :The nickname "..nick.." is already in use"); | 127 session.send(":"..session.host.." 433 * "..nick.." :The nickname "..nick.." is already in use"); |
| 58 return; | 128 return; |
| 59 end | 129 end |
| 130 local full_jid = jid.join(nick, component_jid, "ircd"); | |
| 131 jids[full_jid] = session; | |
| 60 nicks[nick] = session; | 132 nicks[nick] = session; |
| 61 session.nick = nick; | 133 session.nick = nick; |
| 62 session.full_jid = nick.."@"..module.host.."/ircd"; | 134 session.full_jid = full_jid; |
| 63 session.type = "c2s"; | 135 session.type = "c2s"; |
| 64 module:log("debug", "Client bound to %s", session.full_jid); | |
| 65 session.send(":"..session.host.." 001 "..session.nick.." :Welcome to XMPP via the "..session.host.." gateway "..session.nick); | 136 session.send(":"..session.host.." 001 "..session.nick.." :Welcome to XMPP via the "..session.host.." gateway "..session.nick); |
| 66 end | 137 end |
| 67 | 138 |
| 68 local joined_mucs = {}; | 139 local joined_mucs = {}; |
| 69 function commands.JOIN(session, channel) | 140 function commands.JOIN(session, channel) |
| 141 if not session.nick then | |
| 142 return ":"..session.host.." 451 :You have not registered"; | |
| 143 end | |
| 144 | |
| 70 if not joined_mucs[channel] then | 145 if not joined_mucs[channel] then |
| 71 joined_mucs[channel] = { occupants = {}, sessions = {} }; | 146 joined_mucs[channel] = { occupants = {}, sessions = {} }; |
| 72 end | 147 end |
| 73 joined_mucs[channel].sessions[session] = true; | 148 joined_mucs[channel].sessions[session] = true; |
| 74 local join_stanza = st.presence({ from = session.full_jid, to = channel:gsub("^#", "").."@"..conference_server.."/"..session.nick }); | 149 local room_jid = irc2muc(channel); |
| 75 core_process_stanza(session, join_stanza); | 150 print(session.full_jid); |
| 151 local room, err = c:join_room(room_jid, session.nick, { source = session.full_jid } ); | |
| 152 if not room then | |
| 153 return ":"..session.host.." ERR :Could not join room: "..err | |
| 154 end | |
| 155 session.rooms[channel] = room; | |
| 156 room.channel = channel; | |
| 157 room.session = session; | |
| 76 session.send(":"..session.nick.." JOIN :"..channel); | 158 session.send(":"..session.nick.." JOIN :"..channel); |
| 77 session.send(":"..session.host.." 332 "..session.nick.." "..channel.." :Connection in progress..."); | 159 session.send(":"..session.host.." 332 "..session.nick.." "..channel.." :Connection in progress..."); |
| 78 local nicks = session.nick; | 160 room:hook("message", function(event) |
| 79 for nick in pairs(joined_mucs[channel].occupants) do | 161 if not event.body then return end |
| 80 nicks = nicks.." "..nick; | 162 local nick, body = event.nick, event.body; |
| 81 end | 163 if nick ~= session.nick then |
| 82 session.send(":"..session.host.." 353 "..session.nick.." = "..channel.." :"..nicks); | 164 if body:sub(1,4) == "/me " then |
| 165 body = "\1ACTION ".. body:sub(5) .. "\1" | |
| 166 end | |
| 167 session.send(":"..nick.." PRIVMSG "..channel.." :"..body); | |
| 168 end | |
| 169 end); | |
| 170 end | |
| 171 | |
| 172 c:hook("groupchat/joined", function(room) | |
| 173 local session = room.session or jids[room.opts.source]; | |
| 174 local channel = muc2irc(room.jid); | |
| 175 local nicks = session.nick; | |
| 176 -- TODO Break this out into commands.NAMES | |
| 177 for nick in pairs(room.occupants) do | |
| 178 if nick ~= session.nick then | |
| 179 nicks = nicks.." "..nick; | |
| 180 end | |
| 181 end | |
| 83 session.send(":"..session.host.." 366 "..session.nick.." "..channel.." :End of /NAMES list."); | 182 session.send(":"..session.host.." 366 "..session.nick.." "..channel.." :End of /NAMES list."); |
| 84 end | 183 session.send(":"..session.host.." 353 "..session.nick.." = "..channel.." :"..nicks); |
| 184 room:hook("occupant-joined", function(nick) | |
| 185 session.send(":"..nick.nick.."!"..nick.nick.." JOIN :"..room.channel); | |
| 186 end); | |
| 187 room:hook("occupant-left", function(nick) | |
| 188 session.send(":"..nick.nick.."!"..nick.nick.." PART "..room.channel.." :"); | |
| 189 end); | |
| 190 end); | |
| 85 | 191 |
| 86 function commands.PART(session, channel) | 192 function commands.PART(session, channel) |
| 87 local channel, part_message = channel:match("^([^:]+):?(.*)$"); | 193 local channel, part_message = channel:match("^([^:]+):?(.*)$"); |
| 88 channel = channel:match("^([%S]*)"); | 194 channel = channel:match("^([%S]*)"); |
| 89 core_process_stanza(session, st.presence{ type = "unavailable", from = session.full_jid, | 195 session.rooms[channel]:leave(part_message); |
| 90 to = channel:gsub("^#", "").."@"..conference_server.."/"..session.nick }:tag("status"):text(part_message)); | |
| 91 session.send(":"..session.nick.." PART :"..channel); | 196 session.send(":"..session.nick.." PART :"..channel); |
| 92 end | 197 end |
| 93 | 198 |
| 94 function commands.PRIVMSG(session, message) | 199 function commands.PRIVMSG(session, message) |
| 95 local who, message = message:match("^(%S+) :(.+)$"); | 200 local channel, message = message:match("^(%S+) :(.+)$"); |
| 96 if joined_mucs[who] then | 201 if message and #message > 0 and session.rooms[channel] then |
| 97 core_process_stanza(session, st.message{to=who:gsub("^#", "").."@"..conference_server, type="groupchat"}:tag("body"):text(message)); | 202 if message:sub(1,8) == "\1ACTION " then |
| 203 message = "/me ".. message:sub(9,-2) | |
| 204 end | |
| 205 module:log("debug", "%s sending PRIVMSG \"%s\" to %s", session.nick, message, channel); | |
| 206 session.rooms[channel]:send_message(message); | |
| 98 end | 207 end |
| 99 end | 208 end |
| 100 | 209 |
| 101 function commands.PING(session, server) | 210 function commands.PING(session, server) |
| 102 session.send(":"..session.host..": PONG "..server); | 211 session.send(":"..session.host..": PONG "..server); |
| 103 end | 212 end |
| 104 | 213 |
| 105 function commands.WHO(session, channel) | 214 function commands.WHO(session, channel) |
| 106 if joined_mucs[channel] then | 215 if session.rooms[channel] then |
| 107 for nick in pairs(joined_mucs[channel].occupants) do | 216 local room = session.rooms[channel] |
| 217 for nick in pairs(room.occupants) do | |
| 108 --n=MattJ 91.85.191.50 irc.freenode.net MattJ H :0 Matthew Wild | 218 --n=MattJ 91.85.191.50 irc.freenode.net MattJ H :0 Matthew Wild |
| 109 session.send(":"..session.host.." 352 "..session.nick.." "..channel.." "..nick.." "..nick.." "..session.host.." "..nick.." H :0 "..nick); | 219 session.send(":"..session.host.." 352 "..session.nick.." "..channel.." "..nick.." "..nick.." "..session.host.." "..nick.." H :0 "..nick); |
| 110 end | 220 end |
| 111 session.send(":"..session.host.." 315 "..session.nick.." "..channel.. " :End of /WHO list"); | 221 session.send(":"..session.host.." 315 "..session.nick.." "..channel.. " :End of /WHO list"); |
| 112 end | 222 end |
| 114 | 224 |
| 115 function commands.MODE(session, channel) | 225 function commands.MODE(session, channel) |
| 116 session.send(":"..session.host.." 324 "..session.nick.." "..channel.." +J"); | 226 session.send(":"..session.host.." 324 "..session.nick.." "..channel.." +J"); |
| 117 end | 227 end |
| 118 | 228 |
| 119 --- Component (handle stanzas from the server for IRC clients) | 229 |
| 120 function irc_component(origin, stanza) | 230 function commands.RAW(session, data) |
| 121 local from, from_bare = stanza.attr.from, jid.bare(stanza.attr.from); | 231 --c:send(data) |
| 122 local from_node = "#"..jid.split(stanza.attr.from); | 232 end |
| 123 | 233 |
| 124 if joined_mucs[from_node] and from_bare == from then | 234 --c:hook("ready", function () |
| 125 -- From room itself | 235 require "net.connlisteners".register("irc", irc_listener); |
| 126 local joined_muc = joined_mucs[from_node]; | 236 require "net.connlisteners".start("irc"); |
| 127 if stanza.name == "message" then | 237 --end); |
| 128 local subject = stanza:get_child("subject"); | 238 |
| 129 subject = subject and (subject:get_text() or ""); | 239 --print("Starting loop...") |
| 130 if subject then | 240 --verse.loop() |
| 131 for session in pairs(joined_muc.sessions) do | 241 |
| 132 session.send(":"..session.host.." 332 "..session.nick.." "..from_node.." :"..subject); | 242 --[[ TODO |
| 133 end | 243 |
| 134 end | 244 This is so close to working as a Prosody plugin you know ^^ |
| 135 end | 245 Zash: :D |
| 136 elseif joined_mucs[from_node] then | 246 MattJ: That component function can go |
| 137 -- From room occupant | 247 Prosody fires events now |
| 138 local joined_muc = joined_mucs[from_node]; | 248 but verse fires "message" where Prosody fires "message/bare" |
| 139 local nick = select(3, jid.split(from)):gsub(" ", "_"); | 249 [20:59:50] |
| 140 if stanza.name == "presence" then | 250 Easy... don't connect_component |
| 141 local what; | 251 hook "message/*" and presence, and whatever |
| 142 if not stanza.attr.type then | 252 and call c:event("message", ...) |
| 143 if joined_muc.occupants[nick] then | 253 module:hook("message/bare", function (e) c:event("message", e.stanza) end) |
| 144 return; | 254 as an example |
| 145 end | 255 That's so bad ^^ |
| 146 joined_muc.occupants[nick] = true; | 256 and override c:send() to core_post_stanza... |
| 147 what = "JOIN"; | 257 |
| 148 else | 258 --]] |
| 149 joined_muc.occupants[nick] = nil; | 259 |
| 150 what = "PART"; | |
| 151 end | |
| 152 for session in pairs(joined_muc.sessions) do | |
| 153 if nick ~= session.nick then | |
| 154 session.send(":"..nick.."!"..nick.." "..what.." :"..from_node); | |
| 155 end | |
| 156 end | |
| 157 elseif stanza.name == "message" then | |
| 158 local body = stanza:get_child("body"); | |
| 159 body = body and body:get_text() or ""; | |
| 160 local hasdelay = stanza:get_child("delay", "urn:xmpp:delay"); | |
| 161 if body ~= "" and nick then | |
| 162 local to_nick = jid.split(stanza.attr.to); | |
| 163 local session = nicks[to_nick]; | |
| 164 if nick ~= session.nick or hasdelay then | |
| 165 session.send(":"..nick.." PRIVMSG "..from_node.." :"..body); | |
| 166 end | |
| 167 end | |
| 168 if not nick then | |
| 169 module:log("error", "Invalid nick from JID: %s", from); | |
| 170 end | |
| 171 end | |
| 172 end | |
| 173 end | |
| 174 | |
| 175 local function stanza_handler(event) | |
| 176 irc_component(event.origin, event.stanza); | |
| 177 return true; | |
| 178 end | |
| 179 module:hook("iq/bare", stanza_handler, -1); | |
| 180 module:hook("message/bare", stanza_handler, -1); | |
| 181 module:hook("presence/bare", stanza_handler, -1); | |
| 182 module:hook("iq/full", stanza_handler, -1); | |
| 183 module:hook("message/full", stanza_handler, -1); | |
| 184 module:hook("presence/full", stanza_handler, -1); | |
| 185 module:hook("iq/host", stanza_handler, -1); | |
| 186 module:hook("message/host", stanza_handler, -1); | |
| 187 module:hook("presence/host", stanza_handler, -1); | |
| 188 require "core.componentmanager".register_component(module.host, function() end); -- COMPAT Prosody 0.7 | |
| 189 | |
| 190 prosody.events.add_handler("server-stopping", function (shutdown) | |
| 191 module:log("debug", "Closing IRC connections prior to shutdown"); | |
| 192 for channel, joined_muc in pairs(joined_mucs) do | |
| 193 for session in pairs(joined_muc.sessions) do | |
| 194 core_process_stanza(session, | |
| 195 st.presence{ type = "unavailable", from = session.full_jid, | |
| 196 to = channel:gsub("^#", "").."@"..conference_server.."/"..session.nick } | |
| 197 :tag("status") | |
| 198 :text("Connection closed: Server is shutting down"..(shutdown.reason and (": "..shutdown.reason) or ""))); | |
| 199 session:close(); | |
| 200 end | |
| 201 end | |
| 202 end); | |
| 203 | |
| 204 require "net.connlisteners".register("irc", irc_listener); | |
| 205 require "net.connlisteners".start("irc", { port = module:get_option("port") }); |
