tkellem 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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