atig 0.0.1

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 (171) hide show
  1. data/.gitignore +24 -0
  2. data/Gemfile +3 -0
  3. data/README.mkdn +52 -0
  4. data/Rakefile +15 -0
  5. data/atig.gemspec +25 -0
  6. data/bin/atig +74 -0
  7. data/docs/OMakefile +32 -0
  8. data/docs/OMakeroot +45 -0
  9. data/docs/_static/allow.png +0 -0
  10. data/docs/_static/emacs.png +0 -0
  11. data/docs/_static/irc_setting.png +0 -0
  12. data/docs/_static/irssi.png +0 -0
  13. data/docs/_static/limechat.png +0 -0
  14. data/docs/_static/limechat_s.png +0 -0
  15. data/docs/_static/oauth_channel.png +0 -0
  16. data/docs/_static/screenshot.png +0 -0
  17. data/docs/_static/structure.png +0 -0
  18. data/docs/_static/verify.png +0 -0
  19. data/docs/changelog.rst +96 -0
  20. data/docs/commandline_options.rst +21 -0
  21. data/docs/commands.rst +84 -0
  22. data/docs/conf.py +194 -0
  23. data/docs/config.rst +159 -0
  24. data/docs/feature.rst +41 -0
  25. data/docs/graphics.graffle +1995 -0
  26. data/docs/hacking_guide.rst +43 -0
  27. data/docs/index.rst +109 -0
  28. data/docs/irc.rst +31 -0
  29. data/docs/options.rst +75 -0
  30. data/docs/quickstart.rst +89 -0
  31. data/docs/resize.sh +7 -0
  32. data/docs/tiarra.rst +2 -0
  33. data/docs/tig.rst +21 -0
  34. data/lib/atig.rb +19 -0
  35. data/lib/atig/agent.rb +8 -0
  36. data/lib/atig/agent/agent.rb +38 -0
  37. data/lib/atig/agent/clenup.rb +23 -0
  38. data/lib/atig/agent/dm.rb +35 -0
  39. data/lib/atig/agent/following.rb +45 -0
  40. data/lib/atig/agent/full_list.rb +20 -0
  41. data/lib/atig/agent/list.rb +55 -0
  42. data/lib/atig/agent/list_status.rb +46 -0
  43. data/lib/atig/agent/mention.rb +13 -0
  44. data/lib/atig/agent/other_list.rb +18 -0
  45. data/lib/atig/agent/own_list.rb +18 -0
  46. data/lib/atig/agent/stream_follow.rb +38 -0
  47. data/lib/atig/agent/timeline.rb +13 -0
  48. data/lib/atig/agent/user_stream.rb +31 -0
  49. data/lib/atig/basic_twitter.rb +116 -0
  50. data/lib/atig/bitly.rb +52 -0
  51. data/lib/atig/channel.rb +5 -0
  52. data/lib/atig/channel/channel.rb +17 -0
  53. data/lib/atig/channel/dm.rb +14 -0
  54. data/lib/atig/channel/list.rb +76 -0
  55. data/lib/atig/channel/mention.rb +20 -0
  56. data/lib/atig/channel/retweet.rb +28 -0
  57. data/lib/atig/channel/timeline.rb +74 -0
  58. data/lib/atig/command.rb +21 -0
  59. data/lib/atig/command/autofix.rb +58 -0
  60. data/lib/atig/command/command.rb +24 -0
  61. data/lib/atig/command/command_helper.rb +95 -0
  62. data/lib/atig/command/destroy.rb +44 -0
  63. data/lib/atig/command/dm.rb +31 -0
  64. data/lib/atig/command/favorite.rb +27 -0
  65. data/lib/atig/command/info.rb +50 -0
  66. data/lib/atig/command/limit.rb +15 -0
  67. data/lib/atig/command/location.rb +23 -0
  68. data/lib/atig/command/name.rb +18 -0
  69. data/lib/atig/command/option.rb +37 -0
  70. data/lib/atig/command/refresh.rb +18 -0
  71. data/lib/atig/command/reply.rb +37 -0
  72. data/lib/atig/command/retweet.rb +63 -0
  73. data/lib/atig/command/search.rb +51 -0
  74. data/lib/atig/command/spam.rb +26 -0
  75. data/lib/atig/command/status.rb +41 -0
  76. data/lib/atig/command/thread.rb +44 -0
  77. data/lib/atig/command/time.rb +32 -0
  78. data/lib/atig/command/uptime.rb +32 -0
  79. data/lib/atig/command/user.rb +42 -0
  80. data/lib/atig/command/user_info.rb +27 -0
  81. data/lib/atig/command/version.rb +49 -0
  82. data/lib/atig/command/whois.rb +39 -0
  83. data/lib/atig/db/db.rb +60 -0
  84. data/lib/atig/db/followings.rb +131 -0
  85. data/lib/atig/db/listenable.rb +22 -0
  86. data/lib/atig/db/lists.rb +76 -0
  87. data/lib/atig/db/roman.rb +30 -0
  88. data/lib/atig/db/sized_uniq_array.rb +62 -0
  89. data/lib/atig/db/sql.rb +35 -0
  90. data/lib/atig/db/statuses.rb +147 -0
  91. data/lib/atig/db/transaction.rb +47 -0
  92. data/lib/atig/exception_util.rb +26 -0
  93. data/lib/atig/gateway.rb +62 -0
  94. data/lib/atig/gateway/channel.rb +99 -0
  95. data/lib/atig/gateway/session.rb +326 -0
  96. data/lib/atig/http.rb +95 -0
  97. data/lib/atig/ifilter.rb +7 -0
  98. data/lib/atig/ifilter/expand_url.rb +74 -0
  99. data/lib/atig/ifilter/retweet.rb +14 -0
  100. data/lib/atig/ifilter/retweet_time.rb +16 -0
  101. data/lib/atig/ifilter/sanitize.rb +18 -0
  102. data/lib/atig/ifilter/strip.rb +15 -0
  103. data/lib/atig/ifilter/utf7.rb +26 -0
  104. data/lib/atig/ifilter/xid.rb +36 -0
  105. data/lib/atig/levenshtein.rb +49 -0
  106. data/lib/atig/monkey.rb +4 -0
  107. data/lib/atig/oauth-patch.rb +40 -0
  108. data/lib/atig/oauth.rb +55 -0
  109. data/lib/atig/ofilter.rb +4 -0
  110. data/lib/atig/ofilter/escape_url.rb +102 -0
  111. data/lib/atig/ofilter/footer.rb +20 -0
  112. data/lib/atig/ofilter/geo.rb +17 -0
  113. data/lib/atig/ofilter/short_url.rb +47 -0
  114. data/lib/atig/option.rb +90 -0
  115. data/lib/atig/scheduler.rb +79 -0
  116. data/lib/atig/search.rb +22 -0
  117. data/lib/atig/search_twitter.rb +21 -0
  118. data/lib/atig/sized_hash.rb +33 -0
  119. data/lib/atig/stream.rb +66 -0
  120. data/lib/atig/twitter.rb +79 -0
  121. data/lib/atig/twitter_struct.rb +63 -0
  122. data/lib/atig/unu.rb +27 -0
  123. data/lib/atig/update_checker.rb +53 -0
  124. data/lib/atig/url_escape.rb +62 -0
  125. data/lib/atig/util.rb +16 -0
  126. data/lib/atig/version.rb +3 -0
  127. data/lib/memory_profiler.rb +77 -0
  128. data/spec/command/autofix_spec.rb +35 -0
  129. data/spec/command/destroy_spec.rb +98 -0
  130. data/spec/command/dm_spec.rb +28 -0
  131. data/spec/command/favorite_spec.rb +55 -0
  132. data/spec/command/limit_spec.rb +27 -0
  133. data/spec/command/location_spec.rb +25 -0
  134. data/spec/command/name_spec.rb +19 -0
  135. data/spec/command/option_spec.rb +133 -0
  136. data/spec/command/refresh_spec.rb +22 -0
  137. data/spec/command/reply_spec.rb +79 -0
  138. data/spec/command/retweet_spec.rb +66 -0
  139. data/spec/command/spam_spec.rb +27 -0
  140. data/spec/command/status_spec.rb +44 -0
  141. data/spec/command/thread_spec.rb +91 -0
  142. data/spec/command/time_spec.rb +52 -0
  143. data/spec/command/uptime_spec.rb +55 -0
  144. data/spec/command/user_info_spec.rb +42 -0
  145. data/spec/command/user_spec.rb +50 -0
  146. data/spec/command/version_spec.rb +67 -0
  147. data/spec/command/whois_spec.rb +78 -0
  148. data/spec/db/followings_spec.rb +100 -0
  149. data/spec/db/listenable_spec.rb +32 -0
  150. data/spec/db/lists_spec.rb +104 -0
  151. data/spec/db/roman_spec.rb +17 -0
  152. data/spec/db/sized_uniq_array_spec.rb +63 -0
  153. data/spec/db/statuses_spec.rb +180 -0
  154. data/spec/ifilter/expand_url_spec.rb +44 -0
  155. data/spec/ifilter/retweet_spec.rb +28 -0
  156. data/spec/ifilter/retweet_time_spec.rb +25 -0
  157. data/spec/ifilter/sanitize_spec.rb +25 -0
  158. data/spec/ifilter/sid_spec.rb +29 -0
  159. data/spec/ifilter/strip_spec.rb +23 -0
  160. data/spec/ifilter/tid_spec.rb +29 -0
  161. data/spec/ifilter/utf7_spec.rb +30 -0
  162. data/spec/levenshtein_spec.rb +24 -0
  163. data/spec/ofilter/escape_url_spec.rb +50 -0
  164. data/spec/ofilter/footer_spec.rb +32 -0
  165. data/spec/ofilter/geo_spec.rb +33 -0
  166. data/spec/ofilter/short_url_spec.rb +127 -0
  167. data/spec/option_spec.rb +91 -0
  168. data/spec/sized_hash_spec.rb +45 -0
  169. data/spec/spec_helper.rb +35 -0
  170. data/spec/update_checker_spec.rb +55 -0
  171. metadata +326 -0
@@ -0,0 +1,41 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+ require 'atig/command/command'
3
+ begin
4
+ require 'jcode'
5
+ rescue LoadError
6
+ end
7
+
8
+ module Atig
9
+ module Command
10
+ class Status < Atig::Command::Command
11
+ def initialize(*args); super end
12
+ def command_name; %w(status) end
13
+
14
+ def action(target, mesg, command, args)
15
+ if args.empty?
16
+ yield "/me #{command} blah blah"
17
+ return
18
+ end
19
+ text = mesg.split(" ", 2)[1]
20
+ previous,*_ = db.statuses.find_by_user( db.me, :limit => 1)
21
+ if previous and
22
+ ((::Time.now - ::Time.parse(previous.status.created_at)).to_i < 60*60*24 rescue true) and
23
+ text.strip == previous.status.text.strip
24
+ yield "You can't submit the same status twice in a row."
25
+ return
26
+ end
27
+ q = gateway.output_message(:status => text)
28
+
29
+ if q[:status].each_char.to_a.size > 140 then
30
+ yield "You can't submit the status over 140 chars"
31
+ return
32
+ end
33
+
34
+ api.delay(0, :retry=>3) do|t|
35
+ ret = t.post("statuses/update", q)
36
+ gateway.update_status ret,target
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,44 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+ require 'atig/command/command'
3
+ require 'atig/command/info'
4
+
5
+ module Atig
6
+ module Command
7
+ class Thread < Atig::Command::Command
8
+ def initialize(*args)
9
+ super
10
+ end
11
+
12
+ def command_name; %w(thread) end
13
+
14
+ def action(target, mesg, command, args)
15
+ if args.empty?
16
+ yield "/me #{command} <ID> [<NUM>]"
17
+ return
18
+ end
19
+
20
+ tid, num = args
21
+ count = 10 unless (1..20).include?(count = num.to_i)
22
+
23
+ if entry = Info.find_status(db, tid) then
24
+ chain(entry,count){|x|
25
+ gateway[target].message x, Net::IRC::Constants::NOTICE
26
+ }
27
+ else
28
+ yield "No such ID : #{tid}"
29
+ end
30
+ end
31
+
32
+ def chain(entry,count, &f)
33
+ if count <= 0 then
34
+ return
35
+ elsif id = entry.status.in_reply_to_status_id then
36
+ Info.status(db, api, id){|next_|
37
+ chain(next_, count - 1, &f)
38
+ }
39
+ end
40
+ f.call entry
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ require 'time'
4
+ require 'atig/command/command'
5
+
6
+ module Atig
7
+ module Command
8
+ class Time < Atig::Command::Command
9
+ def command_name; %w(time) end
10
+
11
+ def action(target, mesg, command, args)
12
+ if args.empty?
13
+ yield "/me #{command} <NICK>"
14
+ return
15
+ end
16
+ nick, *_ = args
17
+ Info.user(db, api, nick){|user|
18
+ offset = user.utc_offset
19
+ time = "TIME :%s%s (%s)" % [
20
+ (::Time.now + offset).utc.iso8601[0, 19],
21
+ "%+.2d:%.2d" % (offset/60).divmod(60),
22
+ user.time_zone
23
+ ]
24
+ entry = TwitterStruct.make('user' => user,
25
+ 'status' => { 'text' =>
26
+ Net::IRC.ctcp_encode(time) })
27
+ gateway[target].message entry, Net::IRC::Constants::NOTICE
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ require 'atig/command/command'
4
+
5
+ module Atig
6
+ module Command
7
+ class Uptime < Atig::Command::Command
8
+ def initialize(*args)
9
+ super
10
+ @time = ::Time.now
11
+ end
12
+
13
+ def command_name; "uptime" end
14
+
15
+ def action(target, mesg, command, args)
16
+ yield format(::Time.now - @time)
17
+ end
18
+
19
+ def format(x)
20
+ day , y = x.divmod(60*60*24)
21
+ hour, z = y.divmod(60*60)
22
+ min , sec = z.divmod(60)
23
+
24
+ s = ""
25
+ s += "#{day} days " if day > 0
26
+ s += "%02d:" % hour if hour > 0
27
+ s += "%02d:%02d" % [min,sec]
28
+ s
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ require 'time'
4
+ require 'atig/command/command'
5
+
6
+ module Atig
7
+ module Command
8
+ class User < Atig::Command::Command
9
+ def initialize(*args); super end
10
+ def command_name; %w(user u) end
11
+
12
+ def action(target, mesg, command, args)
13
+ if args.empty?
14
+ yield "/me #{command} <NICK> [<NUM>]"
15
+ return
16
+ end
17
+ nick, num,*_ = args
18
+
19
+ count = 20 unless (1..200).include?(count = num.to_i)
20
+ api.delay(0) do|t|
21
+ begin
22
+ statuses = t.get("statuses/user_timeline",
23
+ { :count => count, :screen_name => nick})
24
+ statuses.reverse_each do|status|
25
+ db.statuses.transaction do|d|
26
+ d.add :status => status, :user => status.user, :source => :user
27
+ end
28
+ end
29
+
30
+ db.statuses.
31
+ find_by_screen_name(nick, :limit=>count).
32
+ reverse_each do|entry|
33
+ gateway[target].message entry, Net::IRC::Constants::NOTICE
34
+ end
35
+ rescue Twitter::APIFailed => e
36
+ yield e.to_s
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,27 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ require 'atig/command/command'
4
+
5
+ module Atig
6
+ module Command
7
+ class UserInfo < Atig::Command::Command
8
+ def initialize(*args); super end
9
+ def command_name; %w(bio userinfo) end
10
+
11
+ def action(target, mesg, command,args)
12
+ if args.empty?
13
+ yield "/me #{command} <ID>"
14
+ return
15
+ end
16
+ nick,*_ = args
17
+
18
+ Info.user(db, api, nick)do|user|
19
+ entry = TwitterStruct.make('user' => user,
20
+ 'status' => { 'text' =>
21
+ Net::IRC.ctcp_encode(user.description) })
22
+ gateway[target].message entry, Net::IRC::Constants::NOTICE
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ require 'atig/twitter'
4
+ require 'atig/command/command'
5
+
6
+ module Atig
7
+ module Command
8
+ class Version < Atig::Command::Command
9
+ def command_name; %w(version) end
10
+
11
+ def action(target, mesg, command,args)
12
+ if args.empty?
13
+ yield "/me #{command} <ID>"
14
+ return
15
+ end
16
+ nick,*_ = args
17
+
18
+ entries = db.statuses.find_by_screen_name(nick, :limit => 1)
19
+ if entries && !entries.empty? then
20
+ entry = TwitterStruct.make('user' => entries.first.user,
21
+ 'status' => { 'text' =>
22
+ format(entries.first.status.source) })
23
+ gateway[target].message entry, Net::IRC::Constants::NOTICE
24
+ else
25
+ api.delay(0) do|t|
26
+ begin
27
+ user = t.get("users/show", { :screen_name => nick})
28
+ db.statuses.transaction do|d|
29
+ d.add :user => user, :status => user.status, :source => :version
30
+ entry = TwitterStruct.make('user' => user,
31
+ 'status' => { 'text' =>
32
+ format(user.status.source) })
33
+ gateway[target].message entry, Net::IRC::Constants::NOTICE
34
+ end
35
+ rescue Twitter::APIFailed => e
36
+ yield e.to_s
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ def format(source)
43
+ version = source.gsub(/<[^>]*>/, "").strip
44
+ version << " <#{$1}>" if source =~ / href="([^"]+)/
45
+ Net::IRC.ctcp_encode version
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+ require 'atig/command/info'
3
+ require 'time'
4
+ module Atig
5
+ module Command
6
+ class Whois < Atig::Command::Command
7
+ def command_name; %w(whois) end
8
+
9
+ def action(target, mesg, command,args)
10
+ if args.empty?
11
+ yield "/me #{command} <ID>"
12
+ return
13
+ end
14
+ nick,*_ = args
15
+
16
+ Atig::Command::Info::user(db, api, nick) do|user|
17
+ id = "id=#{user.id}"
18
+ host = "twitter.com"
19
+ host += "/protected" if user.protected
20
+ desc = user.name
21
+ desc = "#{desc} / #{user.description}".gsub(/\s+/, " ") if user.description and not user.description.empty?
22
+ signon_at = ::Time.parse(user.created_at).to_i rescue 0
23
+ idle_sec = (::Time.now - (user.status ? ::Time.parse(user.status.created_at) : signon_at)).to_i rescue 0
24
+ location = user.location
25
+ location = "SoMa neighborhood of San Francisco, CA" if location.nil? or location.empty?
26
+
27
+ send Net::IRC::Constants::RPL_WHOISUSER, nick, id, host, "*", desc
28
+ send Net::IRC::Constants::RPL_WHOISSERVER, nick, host, location
29
+ send Net::IRC::Constants::RPL_WHOISIDLE, nick, "#{idle_sec}", "#{signon_at}", "seconds idle, signon time"
30
+ send Net::IRC::Constants::RPL_ENDOFWHOIS, nick, "End of WHOIS list"
31
+ end
32
+ end
33
+
34
+ def send(command,nick,*params)
35
+ gateway.post gateway.server_name, command, db.me.screen_name, nick, *params
36
+ end
37
+ end
38
+ end
39
+ end
data/lib/atig/db/db.rb ADDED
@@ -0,0 +1,60 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ require 'atig/db/followings'
4
+ require 'atig/db/statuses'
5
+ require 'atig/db/lists'
6
+ require 'atig/util'
7
+ require 'thread'
8
+ require 'set'
9
+ require 'fileutils'
10
+ require 'tmpdir'
11
+
12
+ module Atig
13
+ module Db
14
+ class Db
15
+ include Util
16
+ attr_reader :followings, :statuses, :dms, :lists
17
+ attr_accessor :me
18
+ Path = ::Dir.tmpdir
19
+ VERSION = 4
20
+
21
+ def initialize(context, opt={})
22
+ @log = context.log
23
+ @me = opt[:me]
24
+
25
+ @followings = Followings.new dir('following')
26
+ @statuses = Statuses.new dir('status')
27
+ @dms = Statuses.new dir('dm')
28
+ @lists = Lists.new dir('lists.%s')
29
+
30
+ log :info, "initialize"
31
+ end
32
+
33
+ def dir(id)
34
+ dir = File.expand_path "atig/#{@me.screen_name}/", Path
35
+ log :debug, "db(#{id}) = #{dir}"
36
+ FileUtils.mkdir_p dir
37
+ File.expand_path "#{id}.#{VERSION}.db", dir
38
+ end
39
+
40
+ def transaction(&f)
41
+ @followings.transaction do|_|
42
+ @statuses.transaction do|_|
43
+ @dms.transaction do|_|
44
+ @lists.transaction do|_|
45
+ f.call self
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def cleanup
53
+ transaction do
54
+ @statuses.cleanup
55
+ @dms.cleanup
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,131 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ require 'atig/db/listenable'
4
+ require 'atig/db/transaction'
5
+ require 'atig/db/sql'
6
+
7
+ module Atig
8
+ module Db
9
+ class Followings
10
+ include Listenable
11
+ include Transaction
12
+
13
+ def initialize(name)
14
+ @db = Sql.new name
15
+
16
+ unless File.exist? name then
17
+ @db.execute do|db|
18
+ db.execute %{create table users (
19
+ id integer primary key,
20
+ screen_name text,
21
+ user_id text,
22
+ protected bool,
23
+ only bool,
24
+ data blob);}
25
+ db.execute %{
26
+ create index users_screen on users (screen_name);
27
+ }
28
+ end
29
+ end
30
+
31
+ @users = []
32
+ @on_invalidated = lambda{}
33
+ end
34
+
35
+ def size
36
+ @db.execute do|db|
37
+ db.get_first_value('SELECT COUNT(*) FROM users').to_i
38
+ end
39
+ end
40
+
41
+ def empty?
42
+ @db.execute do|db|
43
+ db.get_first_value('SELECT * FROM users LIMIT 1') == nil
44
+ end
45
+ end
46
+
47
+ def invalidate
48
+ @on_invalidated.call
49
+ end
50
+
51
+ def on_invalidated(&f)
52
+ @on_invalidated = f
53
+ end
54
+
55
+ def users
56
+ @db.execute{|db|
57
+ db.execute("SELECT data FROM users").map{|data|
58
+ @db.load data[0]
59
+ }
60
+ }
61
+ end
62
+
63
+ def exists?(db, templ, *args)
64
+ db.get_first_value("SELECT * FROM users WHERE #{templ} LIMIT 1",*args) != nil
65
+ end
66
+
67
+ def may_notify(mode, xs)
68
+ unless xs.empty? then
69
+ notify mode, xs
70
+ end
71
+ end
72
+
73
+ def update(users)
74
+ @db.execute do|db|
75
+ may_notify :join, users.select{|u|
76
+ not exists?(db,
77
+ "screen_name = ?",
78
+ u.screen_name)
79
+ }
80
+
81
+ names = users.map{|u| u.screen_name.inspect }.join(",")
82
+ parts =
83
+ may_notify :part, db.execute(%{SELECT screen_name,data FROM users
84
+ WHERE screen_name NOT IN (#{names})}).map{|_,data|
85
+ @db.load(data)
86
+ }
87
+ db.execute(%{DELETE FROM users
88
+ WHERE screen_name NOT IN (#{names})})
89
+
90
+ may_notify :mode, users.select{|u|
91
+ exists?(db,
92
+ "screen_name = ? AND (protected != ? OR only != ?)",
93
+ u.screen_name, u.protected, u.only)
94
+ }
95
+
96
+ users.each do|user|
97
+ id = db.get_first_value('SELECT id FROM users WHERE user_id = ? LIMIT 1', user.id)
98
+ if id then
99
+ db.execute("UPDATE users SET screen_name = ?, protected = ?, only = ?, data = ? WHERE id = ?",
100
+ user.screen_name,
101
+ user.protected,
102
+ user.only,
103
+ @db.dump(user),
104
+ id)
105
+ else
106
+ db.execute("INSERT INTO users
107
+ VALUES(NULL, :screen_name, :user_id, :protected, :only, :data)",
108
+ :screen_name => user.screen_name,
109
+ :user_id => user.id,
110
+ :protected => user.protected,
111
+ :only => user.only,
112
+ :data => @db.dump(user))
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ def find_by_screen_name(name)
119
+ @db.execute do|db|
120
+ @db.load db.get_first_value('SELECT data FROM users WHERE screen_name = ? LIMIT 1', name)
121
+ end
122
+ end
123
+
124
+ def include?(user)
125
+ @db.execute do|db|
126
+ exists? db,'user_id = ?', user.id
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end