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") });