tkellem 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. data/.gitignore +7 -0
  2. data/Gemfile +4 -0
  3. data/README.md +28 -6
  4. data/Rakefile +14 -5
  5. data/bin/tkellem +2 -107
  6. data/debian/changelog +5 -0
  7. data/debian/compat +1 -0
  8. data/debian/control +18 -0
  9. data/debian/copyright +42 -0
  10. data/debian/docs +1 -0
  11. data/debian/examples +1 -0
  12. data/debian/install +3 -0
  13. data/debian/manpages +1 -0
  14. data/debian/rules +13 -0
  15. data/debian/source/format +1 -0
  16. data/debian/tkellem.1 +11 -0
  17. data/examples/config.yml +2 -29
  18. data/lib/tkellem/bouncer.rb +196 -31
  19. data/lib/tkellem/bouncer_connection.rb +90 -85
  20. data/lib/tkellem/daemon.rb +155 -0
  21. data/lib/tkellem/irc_message.rb +58 -0
  22. data/lib/tkellem/irc_server.rb +8 -121
  23. data/lib/tkellem/migrations/001_init_db.rb +33 -0
  24. data/lib/tkellem/models/host.rb +11 -0
  25. data/lib/tkellem/models/listen_address.rb +12 -0
  26. data/lib/tkellem/models/network.rb +15 -0
  27. data/lib/tkellem/models/network_user.rb +12 -0
  28. data/lib/tkellem/models/user.rb +56 -0
  29. data/lib/tkellem/plugins/backlog.rb +160 -0
  30. data/lib/tkellem/plugins/push_service.rb +152 -0
  31. data/lib/tkellem/socket_server.rb +29 -0
  32. data/lib/tkellem/tkellem_bot.rb +318 -0
  33. data/lib/tkellem/tkellem_server.rb +114 -0
  34. data/lib/tkellem/version.rb +3 -0
  35. data/lib/tkellem.rb +7 -10
  36. data/resources/bot_command_descriptions.yml +36 -0
  37. data/spec/irc_message_spec.rb +47 -0
  38. data/spec/irc_server_spec.rb +60 -0
  39. data/spec/spec_helper.rb +0 -2
  40. data/tkellem.gemspec +20 -47
  41. metadata +118 -22
  42. data/VERSION +0 -1
  43. data/lib/tkellem/backlog.rb +0 -85
  44. data/lib/tkellem/irc_line.rb +0 -58
@@ -1,6 +1,7 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
1
3
  require 'eventmachine'
2
- require 'tkellem/irc_line'
3
- require 'tkellem/backlog'
4
+ require 'tkellem/irc_message'
4
5
 
5
6
  module Tkellem
6
7
 
@@ -8,37 +9,36 @@ module BouncerConnection
8
9
  include EM::Protocols::LineText2
9
10
  include Tkellem::EasyLogger
10
11
 
11
- def initialize(bouncer, do_ssl)
12
+ def initialize(tkellem_server, do_ssl)
12
13
  set_delimiter "\r\n"
13
14
 
14
15
  @ssl = do_ssl
15
- @bouncer = bouncer
16
+ @tkellem = tkellem_server
16
17
 
17
- @irc_server = nil
18
- @backlog = nil
19
- @nick = nil
20
- @conn_name = nil
21
- @name = nil
18
+ @state = :auth
19
+ @name = 'new-conn'
20
+ @data = {}
22
21
  end
23
- attr_reader :ssl, :irc_server, :backlog, :bouncer, :nick
22
+ attr_reader :ssl, :bouncer, :name, :device_name
23
+ alias_method :log_name, :name
24
24
 
25
- def connected?
26
- !!irc_server
25
+ def nick
26
+ @bouncer ? @bouncer.nick : @nick
27
27
  end
28
28
 
29
- def name
30
- @name || "new-conn"
29
+ def data(key)
30
+ @data[key] ||= {}
31
31
  end
32
32
 
33
33
  def post_init
34
34
  if ssl
35
- debug "starting TLS"
36
35
  start_tls :verify_peer => false
36
+ else
37
+ ssl_handshake_completed
37
38
  end
38
39
  end
39
40
 
40
41
  def ssl_handshake_completed
41
- debug "TLS complete"
42
42
  end
43
43
 
44
44
  def error!(msg)
@@ -47,89 +47,98 @@ module BouncerConnection
47
47
  close_connection(true)
48
48
  end
49
49
 
50
- def connect(conn_name, client_name, password)
51
- @irc_server = bouncer.get_irc_server(conn_name.downcase)
52
- unless irc_server && irc_server.connected?
53
- error!("unknown connection #{conn_name}")
54
- return
55
- end
56
-
57
- unless bouncer.do_auth(@nick, @password, irc_server)
58
- error!("bad auth, please check your password")
59
- @irc_server = @conn_name = @name = @backlog = nil
60
- return
61
- end
62
-
63
- @conn_name = conn_name
64
- @name = client_name
65
- @backlog = irc_server.bouncer_connect(self)
66
- unless backlog
67
- error!("unknown client #{client_name}")
68
- @irc_server = @conn_name = @name = nil
69
- return
70
- end
71
-
50
+ def connect_to_irc_server
51
+ @bouncer = @tkellem.find_bouncer(@user, @conn_name)
52
+ return error!("Unknown connection: #{@conn_name}") unless @bouncer
53
+ @state = :connected
72
54
  info "connected"
73
-
74
- irc_server.send_welcome(self)
75
- backlog.send_backlog(self)
76
- irc_server.rooms.each { |room| simulate_join(room) }
55
+ @bouncer.connect_client(self)
77
56
  end
78
57
 
79
- def tkellem(msg)
80
- case msg.args.first
81
- when /nothing_yet/i
82
- else
83
- send_msg(":tkellem!tkellem@tkellem PRIVMSG #{nick} :Unknown tkellem command #{msg.args.first}")
58
+ def msg_tkellem(msg)
59
+ TkellemBot.run_command(msg.args.join(' '), @user) do |response|
60
+ say_as_tkellem(response)
84
61
  end
85
62
  end
86
63
 
64
+ def say_as_tkellem(message)
65
+ send_msg(":-tkellem!~tkellem@tkellem PRIVMSG #{nick} :#{message}")
66
+ end
67
+
87
68
  def receive_line(line)
88
69
  trace "from client: #{line}"
89
- msg = IrcLine.parse(line)
90
- case msg.command
91
- when /tkellem/i
92
- tkellem(msg)
93
- when /pass/i
94
- @password = msg.args.first
95
- when /user/i
96
- @conn_name, @client_name = msg.args.last.strip.split(' ')
97
- when /nick/i
98
- if connected?
99
- irc_server.change_nick(msg.last)
100
- else
101
- @nick = msg.last
102
- connect(@conn_name, @client_name, @password)
70
+ msg = IrcMessage.parse(line)
71
+
72
+ command = msg.command
73
+ if @user && command == 'PRIVMSG' && msg.args.first == '-tkellem'
74
+ msg_tkellem(IrcMessage.new(nil, 'TKELLEM', [msg.args.last]))
75
+ elsif command == 'TKELLEM'
76
+ msg_tkellem(msg)
77
+ elsif command == 'CAP'
78
+ # TODO: full support for CAP -- this just gets mobile colloquy connecting
79
+ if msg.args.first =~ /req/i
80
+ send_msg("CAP NAK")
103
81
  end
104
- when /quit/i
105
- # DENIED
82
+ elsif command == 'PASS'
83
+ unless @password
84
+ @password = msg.args.first
85
+ end
86
+ elsif command == 'NICK' && @state == :auth
87
+ @nick = msg.args.first
88
+ elsif command == 'QUIT'
106
89
  close_connection
107
- when /ping/i
108
- send_msg(":tkellem PONG tkellem :#{msg.last}")
90
+ elsif command == 'USER'
91
+ msg_user(msg)
92
+ elsif @state == :auth
93
+ error!("Protocol error. You must authenticate first.")
94
+ elsif @state == :connected
95
+ @bouncer.client_msg(self, msg)
109
96
  else
110
- if !connected?
111
- close_connection
97
+ say_as_tkellem("You must connect to an irc network to do that.")
98
+ end
99
+
100
+ rescue => e
101
+ error "Error handling message: {#{msg}} #{e}"
102
+ e.backtrace.each { |l| error l }
103
+ begin
104
+ error! "Internal Tkellem error."
105
+ rescue
106
+ end
107
+ end
108
+
109
+ def msg_user(msg)
110
+ unless @user
111
+ @username, rest = msg.args.first.strip.split('@', 2).map { |a| a.downcase }
112
+ @name = @username
113
+ @user = User.authenticate(@username, @password)
114
+ return error!("Unknown username: #{@username} or bad password.") unless @user
115
+
116
+ if rest && !rest.empty?
117
+ @conn_name, @device_name = rest.split(':', 2)
118
+ # 'default' or missing device_name to use the default backlog
119
+ # pass a device_name to have device-independent backlogs
120
+ @device_name = @device_name.presence || 'default'
121
+ @name = "#{@username}-#{@conn_name}"
122
+ @name += "-#{@device_name}" if @device_name
123
+ connect_to_irc_server
112
124
  else
113
- # pay it forward
114
- irc_server.send_msg(msg)
125
+ @name = "#{@username}-console"
126
+ connect_to_tkellem_console
115
127
  end
116
128
  end
117
129
  end
118
130
 
131
+ def connect_to_tkellem_console
132
+ send_msg(":tkellem 001 #{nick} :Welcome to the Tkellem admin console")
133
+ send_msg(":tkellem 376 #{nick} :End")
134
+ @state = :console
135
+ end
136
+
119
137
  def simulate_join(room)
120
- send_msg(":#{irc_server.nick}!#{name}@tkellem JOIN #{room}")
138
+ send_msg(":#{nick}!#{name}@tkellem JOIN #{room}")
121
139
  # TODO: intercept the NAMES response so that only this bouncer gets it
122
140
  # Otherwise other clients might show an "in this room" line.
123
- irc_server.send_msg("NAMES #{room}\r\n")
124
- end
125
-
126
- def transient_response(msg)
127
- send_msg(msg)
128
- if msg.command == "366"
129
- # finished joining this room, let's backlog it
130
- debug "got final NAMES for #{msg.args[1]}, sending backlog"
131
- backlog.send_backlog(self, msg.args[1])
132
- end
141
+ @bouncer.send_msg("NAMES #{room}\r\n")
133
142
  end
134
143
 
135
144
  def send_msg(msg)
@@ -137,12 +146,8 @@ module BouncerConnection
137
146
  send_data("#{msg}\r\n")
138
147
  end
139
148
 
140
- def log_name
141
- "#{@conn_name}-#{name}"
142
- end
143
-
144
149
  def unbind
145
- irc_server.bouncer_disconnect(self) if connected?
150
+ @bouncer.disconnect_client(self) if @bouncer
146
151
  end
147
152
  end
148
153
 
@@ -0,0 +1,155 @@
1
+ require 'fileutils'
2
+ require 'optparse'
3
+
4
+ require 'tkellem'
5
+ require 'tkellem/socket_server'
6
+
7
+ class Tkellem::Daemon
8
+ attr_reader :options
9
+
10
+ def initialize(args)
11
+ @args = args
12
+ @options = {
13
+ :path => File.expand_path("~/.tkellem/"),
14
+ }
15
+ end
16
+
17
+ def run
18
+ OptionParser.new do |opts|
19
+ opts.banner = "Usage #{$0} <command> <options>"
20
+ opts.separator %{\nWhere <command> is one of:
21
+ start start the jobs daemon
22
+ stop stop the jobs daemon
23
+ run start and run in the foreground
24
+ restart stop and then start the jobs daemon
25
+ status show daemon status
26
+ admin run admin commands as if connected to the tkellem console
27
+ }
28
+
29
+ opts.separator "\n<options>"
30
+ opts.on("-p", "--path", "Use alternate folder for tkellem data (default #{options[:path]})") { |p| options[:path] = p }
31
+ opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
32
+ end.parse!(@args)
33
+
34
+ FileUtils.mkdir_p(path)
35
+ File.chmod(0700, path)
36
+ command = @args.shift
37
+ case command
38
+ when 'start'
39
+ daemonize
40
+ start
41
+ when 'stop'
42
+ stop
43
+ when 'run'
44
+ start
45
+ when 'status'
46
+ exit(status ? 0 : 1)
47
+ when 'restart'
48
+ stop if status(false)
49
+ while status(false)
50
+ print "."
51
+ sleep(0.5)
52
+ end
53
+ daemonize
54
+ start
55
+ when 'admin'
56
+ admin
57
+ else
58
+ raise("Unknown command: #{command.inspect}")
59
+ end
60
+ end
61
+
62
+ protected
63
+
64
+ def admin
65
+ require 'socket'
66
+ socket = UNIXSocket.new(socket_file)
67
+ line = @args.join(' ').strip
68
+ if line.empty?
69
+ require 'readline'
70
+ while line = Readline.readline('> ', true)
71
+ admin_command(line, socket)
72
+ end
73
+ else
74
+ admin_command(line, socket)
75
+ end
76
+ end
77
+
78
+ def admin_command(line, socket)
79
+ socket.puts(line)
80
+ loop do
81
+ line = socket.readline("\n").chomp
82
+ puts line
83
+ if line == "\0"
84
+ break
85
+ end
86
+ end
87
+ end
88
+
89
+ def start
90
+ trap("INT") { EM.stop }
91
+ EM.run do
92
+ @admin = EM.start_unix_domain_server(socket_file, Tkellem::SocketServer)
93
+ Tkellem::TkellemServer.new
94
+ end
95
+ ensure
96
+ remove_files
97
+ end
98
+
99
+ def daemonize
100
+ puts "Daemonizing..."
101
+ exit if fork
102
+ Process.setsid
103
+ exit if fork
104
+ @daemon = true
105
+ File.open(pid_file, 'wb') { |f| f.write(Process.pid.to_s) }
106
+
107
+ # TODO: set up logging
108
+ STDIN.reopen("/dev/null")
109
+ STDOUT.reopen("/dev/null")
110
+ STDERR.reopen(STDOUT)
111
+ # STDOUT.sync = STDERR.sync = true
112
+ end
113
+
114
+ def stop
115
+ pid = File.read(pid_file) if File.file?(pid_file)
116
+ if pid.to_i > 0
117
+ puts "Stopping tkellem #{pid}..."
118
+ Process.kill('INT', pid.to_i)
119
+ else
120
+ status
121
+ end
122
+ end
123
+
124
+ def status(print = true)
125
+ pid = File.read(pid_file) if File.file?(pid_file)
126
+ if pid
127
+ puts "tkellem running, pid: #{pid}" if print
128
+ else
129
+ puts "tkellem not running" if print
130
+ end
131
+ pid.to_i > 0 ? pid.to_i : nil
132
+ end
133
+
134
+ def remove_files
135
+ FileUtils.rm(socket_file)
136
+ return unless @daemon
137
+ pid = File.read(pid_file) if File.file?(pid_file)
138
+ if pid.to_i == Process.pid
139
+ FileUtils.rm(pid_file)
140
+ end
141
+ end
142
+
143
+ def path
144
+ options[:path]
145
+ end
146
+
147
+ def pid_file
148
+ File.join(path, 'tkellem.pid')
149
+ end
150
+
151
+ def socket_file
152
+ File.join(path, 'tkellem.socket')
153
+ end
154
+
155
+ end
@@ -0,0 +1,58 @@
1
+ module Tkellem
2
+
3
+ class IrcMessage < Struct.new(:prefix, :command, :args)
4
+ RE = %r{(:[^ ]+ )?([^ ]*)(.*)}i
5
+
6
+ def self.parse(line)
7
+ md = RE.match(line) or raise("invalid input: #{line.inspect}")
8
+
9
+ prefix = md[1] && md[1][1..-1].strip
10
+ command = md[2].upcase
11
+ args = md[3]
12
+
13
+ args.strip!
14
+ idx = args.index(":")
15
+ if idx && (idx == 0 || args[idx-1] == " "[0])
16
+ args = args[0...idx].split(' ') + [args[idx+1..-1]]
17
+ else
18
+ args = args.split(' ')
19
+ end
20
+
21
+ self.new(prefix, command, args)
22
+ end
23
+
24
+ def command?(cmd)
25
+ @command.downcase == cmd.downcase
26
+ end
27
+
28
+ def replay
29
+ line = []
30
+ line << ":#{prefix}" unless prefix.nil?
31
+ line << command
32
+ ext_arg = args.last if args.last && args.last.match(%r{\s})
33
+ line += ext_arg ? args[0...-1] : args
34
+ line << ":#{ext_arg}" unless ext_arg.nil?
35
+ line.join ' '
36
+ end
37
+ alias_method :to_s, :replay
38
+
39
+ def target_user
40
+ if prefix && md = %r{^([^!]+)}.match(prefix)
41
+ md[1]
42
+ else
43
+ nil
44
+ end
45
+ end
46
+
47
+ def with_timestamp(timestamp)
48
+ args = self.args
49
+ if args && args[-1]
50
+ args = args.dup
51
+ args[-1] = "#{timestamp.strftime("%H:%M:%S")}> #{args[-1]}"
52
+ end
53
+ IrcMessage.new(prefix, command, args)
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -1,47 +1,23 @@
1
1
  require 'set'
2
2
  require 'eventmachine'
3
- require 'tkellem/irc_line'
3
+ require 'tkellem/irc_message'
4
4
  require 'tkellem/bouncer_connection'
5
- require 'tkellem/backlog'
6
5
 
7
6
  module Tkellem
8
7
 
9
- module IrcServer
8
+ module IrcServerConnection
10
9
  include EM::Protocols::LineText2
11
10
  include Tkellem::EasyLogger
12
11
 
13
- def initialize(bouncer, name, do_ssl, nick)
12
+ def initialize(bouncer, do_ssl)
14
13
  set_delimiter "\r\n"
15
-
16
14
  @bouncer = bouncer
17
- @name = name
18
15
  @ssl = do_ssl
19
- @nick = nick
20
-
21
- @max_backlog = nil
22
- @connected = false
23
- @welcomes = []
24
- @rooms = Set.new
25
- @backlogs = {}
26
- @active_conns = []
27
- @joined_rooms = false
28
- @pending_rooms = []
29
- end
30
- attr_reader :name, :backlogs, :welcomes, :rooms, :nick, :active_conns
31
- alias_method :log_name, :name
32
-
33
- def connected?
34
- @connected
35
- end
36
-
37
- def set_max_backlog(max_backlog)
38
- @max_backlog = max_backlog
39
- backlogs.each { |name, backlog| backlog.max_backlog = max_backlog }
40
16
  end
41
17
 
42
18
  def post_init
43
19
  if @ssl
44
- debug "starting TLS"
20
+ @bouncer.debug "starting TLS"
45
21
  # TODO: support strict cert checks
46
22
  start_tls :verify_peer => false
47
23
  else
@@ -50,106 +26,17 @@ module IrcServer
50
26
  end
51
27
 
52
28
  def ssl_handshake_completed
53
- # TODO: support sending a real username, realname, etc
54
- send_msg("USER #{nick} localhost blah :#{nick}")
55
- change_nick(nick, true)
29
+ EM.next_tick { @bouncer.connection_established }
56
30
  end
57
31
 
58
32
  def receive_line(line)
59
33
  trace "from server: #{line}"
60
- msg = IrcLine.parse(line)
61
-
62
- case msg.command
63
- when /0\d\d/, /2[56]\d/, /37[256]/
64
- welcomes << msg
65
- got_welcome if msg.command == "376" # end of MOTD
66
- when /join/i
67
- debug "#{msg.target_user} joined #{msg.last}"
68
- rooms << msg.last if msg.target_user == nick
69
- when /part/i
70
- debug "#{msg.target_user} left #{msg.last}"
71
- rooms.delete(msg.last) if msg.target_user == nick
72
- when /ping/i
73
- send_msg("PONG #{nick}!tkellem #{msg.args.first}")
74
- when /pong/i
75
- # swallow it, we handle ping-pong from clients separately, in
76
- # BouncerConnection
77
- else
78
- end
79
-
80
- backlogs.each { |name, backlog| backlog.handle_message(msg) }
81
- end
82
-
83
- def got_welcome
84
- return if @joined_rooms
85
- @joined_rooms = true
86
- @pending_rooms.each do |room|
87
- join_room(room)
88
- end
89
- @pending_rooms.clear
90
-
91
- # We're all initialized, allow connections
92
- @connected = true
93
- end
94
-
95
- def change_nick(new_nick, force = false)
96
- return if !force && new_nick == @nick
97
- @nick = new_nick
98
- send_msg("NICK #{new_nick}")
99
- end
100
-
101
- def join_room(room_name)
102
- if @joined_rooms
103
- send_msg("JOIN #{room_name}")
104
- else
105
- @pending_rooms << room_name
106
- end
107
- end
108
-
109
- def add_client(name)
110
- return if backlogs[name]
111
- backlog = Backlog.new(name, @max_backlog)
112
- backlogs[name] = backlog
113
- end
114
-
115
- def remove_client(name)
116
- backlog = backlogs.delete(name)
117
- if backlog
118
- backlog.active_conns.each do |conn|
119
- conn.error!("client removed")
120
- end
121
- end
122
- end
123
-
124
- def send_msg(msg)
125
- trace "to server: #{msg}"
126
- send_data("#{msg}\r\n")
127
- end
128
-
129
- def send_welcome(bouncer_conn)
130
- welcomes.each { |msg| bouncer_conn.send_msg(msg) }
34
+ msg = IrcMessage.parse(line)
35
+ @bouncer.server_msg(msg)
131
36
  end
132
37
 
133
38
  def unbind
134
- debug "OMG we got disconnected."
135
- # TODO: reconnect if desired. but not if this server was explicitly shut
136
- # down or removed.
137
- backlogs.keys.each { |name| remove_client(name) }
138
- end
139
-
140
- def bouncer_connect(bouncer_conn)
141
- return nil unless backlogs[bouncer_conn.name]
142
-
143
- active_conns << bouncer_conn
144
- backlogs[bouncer_conn.name].add_conn(bouncer_conn)
145
- backlogs[bouncer_conn.name]
146
- end
147
-
148
- def bouncer_disconnect(bouncer_conn)
149
- return nil unless backlogs[bouncer_conn.name]
150
-
151
- backlogs[bouncer_conn.name].remove_conn(bouncer_conn)
152
- active_conns.delete(bouncer_conn)
39
+ @bouncer.disconnected!
153
40
  end
154
41
  end
155
42
 
@@ -0,0 +1,33 @@
1
+ class InitDb < ActiveRecord::Migration
2
+ def self.up
3
+ create_table 'listen_addresses' do |t|
4
+ t.string 'address', :null => false
5
+ t.integer 'port', :null => false
6
+ t.boolean 'ssl', :null => false, :default => false
7
+ end
8
+
9
+ create_table 'users' do |t|
10
+ t.string 'username', :null => false
11
+ t.string 'password'
12
+ t.string 'role', :null => false, :default => 'user'
13
+ end
14
+
15
+ create_table 'networks' do |t|
16
+ t.belongs_to 'user'
17
+ t.string 'name', :null => false
18
+ end
19
+
20
+ create_table 'hosts' do |t|
21
+ t.belongs_to 'network'
22
+ t.string 'address', :null => false
23
+ t.integer 'port', :null => false
24
+ t.boolean 'ssl', :null => false, :default => false
25
+ end
26
+
27
+ create_table 'network_users' do |t|
28
+ t.belongs_to 'user'
29
+ t.belongs_to 'network'
30
+ t.string 'nick'
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,11 @@
1
+ module Tkellem
2
+
3
+ class Host < ActiveRecord::Base
4
+ belongs_to :network
5
+
6
+ def to_s
7
+ "#{ssl ? 'ircs' : 'irc'}://#{address}:#{port}"
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,12 @@
1
+ module Tkellem
2
+
3
+ class ListenAddress < ActiveRecord::Base
4
+ validates_uniqueness_of :port, :scope => [:address]
5
+ validates_presence_of :address, :port
6
+
7
+ def to_s
8
+ "#{ssl ? 'ircs' : 'irc'}://#{address}:#{port}"
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,15 @@
1
+ module Tkellem
2
+
3
+ class Network < ActiveRecord::Base
4
+ has_many :hosts, :dependent => :destroy
5
+ has_many :network_users, :dependent => :destroy
6
+ # networks either belong to a specific user, or they are public and any user
7
+ # can join them.
8
+ belongs_to :user
9
+
10
+ def public?
11
+ !user
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,12 @@
1
+ module Tkellem
2
+
3
+ class NetworkUser < ActiveRecord::Base
4
+ belongs_to :network
5
+ belongs_to :user
6
+
7
+ def nick
8
+ read_attribute(:nick) || user.name
9
+ end
10
+ end
11
+
12
+ end