twitter2jabber 0.0.2 → 0.0.3

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.
data/README CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  == VERSION
4
4
 
5
- This documentation refers to twitter2jabber version 0.0.2
5
+ This documentation refers to twitter2jabber version 0.0.3
6
6
 
7
7
 
8
8
  == DESCRIPTION
data/Rakefile CHANGED
@@ -15,7 +15,7 @@ begin
15
15
  :summary => %q{Twitter-to-Jabber gateway.},
16
16
  :homepage => %q{http://twitter2jabber.rubyforge.org/},
17
17
  :files => FileList['lib/**/*.rb', 'bin/*'].to_a,
18
- :extra_files => FileList['[A-Z]*', 'sample/*'].to_a,
18
+ :extra_files => FileList['[A-Z]*', 'sample/**/*'].to_a,
19
19
  :dependencies => %w[twitter xmpp4r-simple highline]
20
20
  }
21
21
  }}
data/TODO CHANGED
@@ -1,4 +1,4 @@
1
- - persistent cache!
2
- - logging/debug output
3
- - extended tweet format
4
- - interrupt handling in loop mode
1
+ - persistent cache! (friends_timeline: since/since_id?)
2
+ - better interrupt handling (in loop mode)
3
+ - daemonize after asking for credentials (in loop mode)
4
+ - additional commands?
data/bin/twitter2jabber CHANGED
@@ -46,12 +46,16 @@ rescue Interrupt
46
46
  end
47
47
 
48
48
  options = {
49
- :config => 'config.yaml',
50
- :user => nil,
51
- :jid => nil,
52
- :recipients => [],
53
- :loop => false,
54
- :pause => nil
49
+ :config => 'config.yaml',
50
+ :user => nil,
51
+ :jid => nil,
52
+ :recipients => [],
53
+ :formats => nil,
54
+ :template_dir => nil,
55
+ :loop => false,
56
+ :pause => nil,
57
+ :verbose => false,
58
+ :debug => false
55
59
  }
56
60
 
57
61
  OptionParser.new { |opts|
@@ -86,6 +90,17 @@ OptionParser.new { |opts|
86
90
 
87
91
  opts.separator ' '
88
92
 
93
+ opts.on('-f', '--formats LIST', "Comma-separated list of formats [Default: #{Twitter2Jabber::DEFAULT_FORMATS.join(',')}]") { |f|
94
+ options[:formats] = f.split(',')
95
+ }
96
+
97
+ opts.on('-t', '--template-dir PATH', 'Path to custom template directory', "[Default: #{Twitter2Jabber::DEFAULT_TEMPLATES}]") { |t|
98
+ abort "Template directory not found: #{t}" unless File.directory?(t)
99
+ options[:template_dir] = t
100
+ }
101
+
102
+ opts.separator ' '
103
+
89
104
  opts.on('-l', '--loop', 'Run in a loop') {
90
105
  options[:loop] = true
91
106
  }
@@ -97,6 +112,14 @@ OptionParser.new { |opts|
97
112
  opts.separator ' '
98
113
  opts.separator 'Generic options:'
99
114
 
115
+ opts.on('-v', '--verbose', 'Be verbose') {
116
+ options[:verbose] = true
117
+ }
118
+
119
+ opts.on('-D', '--debug', "Print debug output and don't send or update anything") {
120
+ options[:debug] = true
121
+ }
122
+
100
123
  opts.on('-h', '--help', 'Print this help message and exit') {
101
124
  puts opts
102
125
  exit
@@ -129,6 +152,10 @@ config[:jabber][:pass] ||= ask("Password for Jabber ID #{config[:jabber][:user]}
129
152
  recipients = options[:recipients] + (config.delete(:recipients) || [])
130
153
  recipients.uniq!
131
154
 
155
+ [:formats, :template_dir, :verbose, :debug].each { |key|
156
+ config[key] = options[key] if options[key]
157
+ }
158
+
132
159
  begin
133
160
  twitter2jabber = Twitter2Jabber.new(config)
134
161
 
@@ -4,7 +4,7 @@ class Twitter2Jabber
4
4
 
5
5
  MAJOR = 0
6
6
  MINOR = 0
7
- TINY = 2
7
+ TINY = 3
8
8
 
9
9
  class << self
10
10
 
@@ -25,6 +25,7 @@
25
25
  #++
26
26
 
27
27
  require 'time'
28
+ require 'erb'
28
29
 
29
30
  require 'rubygems'
30
31
  require 'twitter'
@@ -34,8 +35,17 @@ require 'twitter2jabber/version'
34
35
 
35
36
  class Twitter2Jabber
36
37
 
38
+ MAX_LENGTH = 140
39
+
37
40
  DEFAULT_PAUSE = 60
38
41
 
42
+ DEFAULT_FORMATS = %w[txt]
43
+
44
+ DEFAULT_TEMPLATES = File.expand_path(File.join(File.dirname(__FILE__), %w[.. sample templates]))
45
+
46
+ JABBER_NS = 'http://jabber.org/protocol/xhtml-im'
47
+ XHTML_NS = 'http://www.w3.org/1999/xhtml'
48
+
39
49
  def self.loop(options, recipients = [], pause = nil, &block)
40
50
  new(options).loop(recipients, pause, &block)
41
51
  end
@@ -44,40 +54,65 @@ class Twitter2Jabber
44
54
  new(options).run(recipients, &block)
45
55
  end
46
56
 
47
- attr_reader :twitter, :jabber, :filter
57
+ attr_reader :id, :verbose, :debug, :twitter, :jabber, :filter, :formats, :templates
48
58
 
49
59
  def initialize(options, &block)
50
60
  [:twitter, :jabber].each { |client|
51
61
  raise ArgumentError, "#{client} config missing" unless options[client].is_a?(Hash)
52
62
  }
53
63
 
64
+ @id = "#{options[:twitter][:user]} -> #{options[:jabber][:user]}"
65
+
66
+ @verbose = options[:verbose]
67
+ @debug = options[:debug]
68
+
54
69
  @twitter = twitter_connect(options[:twitter])
55
70
  @jabber = jabber_connect(options[:jabber])
56
71
 
57
- @filter = options[:filter] || block
72
+ @filter = options[:filter] || block
73
+ @formats = options[:formats] || DEFAULT_FORMATS
74
+
75
+ @templates = Dir[
76
+ File.join(options[:template_dir] || DEFAULT_TEMPLATES, 'tweet.*')
77
+ ].inject({}) { |hash, template|
78
+ hash.update(File.extname(template).sub(/\A\./, '') => File.read(template))
79
+ }
58
80
  end
59
81
 
60
- def run(recipients = [], seen = {}, &block)
61
- deliver_tweets(recipients, seen, &block)
82
+ def run(recipients = [], seen = {}, flag = true, &block)
83
+ deliver_tweets(recipients, seen, &block) if flag
62
84
  post_messages
63
85
  end
64
86
 
65
87
  def loop(recipients = [], pause = nil, &block)
66
88
  pause ||= DEFAULT_PAUSE
67
89
 
68
- seen = Hash.new { |h, k| h[k] = true; false }
90
+ # jabber/twitter ratio
91
+ ratio = 10
92
+ pause /= ratio
69
93
 
70
- Kernel.loop {
71
- run(recipients, seen, &block)
94
+ # sleep at least one second
95
+ pause = 1 if pause < 1
96
+
97
+ i, seen = 0, Hash.new { |h, k| h[k] = true; false }
98
+
99
+ trap(:INT) { i = nil }
100
+
101
+ while i
102
+ i += 1
103
+
104
+ run(recipients, seen, i % ratio == 1, &block)
72
105
 
73
106
  sleep pause
74
- }
107
+ end
75
108
  end
76
109
 
77
110
  def deliver_tweets(recipients, seen = {}, &block)
78
111
  get_tweets.each { |tweet|
79
112
  next if seen[tweet.id]
80
113
 
114
+ logt tweet.id
115
+
81
116
  # apply filters
82
117
  next if filter && !filter[tweet]
83
118
  next if block && !block[tweet]
@@ -85,19 +120,20 @@ class Twitter2Jabber
85
120
  msg = format_tweet(tweet)
86
121
 
87
122
  recipients.each { |recipient|
88
- jabber.deliver(recipient, msg)
123
+ deliver(recipient, msg)
89
124
  }
90
125
 
91
126
  sleep 1
92
127
  }
93
- rescue Twitter::CantConnect
94
- sleep pause
95
- retry
96
128
  end
97
129
 
98
130
  def post_messages
99
131
  jabber.received_messages { |msg|
100
- twitter.update(msg.body) if msg.type == :chat
132
+ next unless msg.type == :chat
133
+
134
+ logj msg.id
135
+
136
+ handle_command(msg.body, msg.from)
101
137
  }
102
138
  end
103
139
 
@@ -109,25 +145,155 @@ class Twitter2Jabber
109
145
 
110
146
  # verify credentials
111
147
  client.verify_credentials
148
+
149
+ logt "connected #{Time.now}"
150
+
112
151
  client
113
152
  rescue Twitter::TwitterError => err
114
153
  raise "Can't connect to Twitter with ID '#{options[:user]}': #{err}"
115
154
  end
116
155
 
117
156
  def jabber_connect(options)
118
- Jabber::Simple.new(options[:user], options[:pass])
157
+ client = Jabber::Simple.new(options[:user], options[:pass])
158
+
159
+ logj "connected #{Time.now}"
160
+
161
+ client
119
162
  rescue Jabber::JabberError => err
120
163
  raise "Can't connect to Jabber with JID '#{options[:user]}': #{err}"
121
164
  end
122
165
 
123
166
  def get_tweets
124
167
  twitter.friends_timeline.sort_by { |tweet|
125
- Time.parse(tweet.created_at)
168
+ tweet.created_at = Time.parse(tweet.created_at)
126
169
  }
170
+ rescue Twitter::CantConnect
171
+ sleep pause
172
+ retry
127
173
  end
128
174
 
129
175
  def format_tweet(tweet)
130
- "#{tweet.user.name} [#{tweet.created_at}]:\n\n#{tweet.text}"
176
+ user = tweet.user
177
+
178
+ msg = Jabber::Message.new.set_type(:chat)
179
+
180
+ formats.each { |format|
181
+ if template = templates[format]
182
+ msg.add_element format_element(format) {
183
+ ERB.new(template).result(binding)
184
+ }
185
+ end
186
+ }
187
+
188
+ msg
189
+ end
190
+
191
+ # cf. <http://devblog.famundo.com/articles/2006/10/18/ruby-and-xmpp-jabber-part-3-adding-html-to-the-messages>
192
+ def format_element(format)
193
+ body = REXML::Element.new('body')
194
+ REXML::Text.new(yield, format != 'html', body, true, nil, /.^/)
195
+
196
+ case format
197
+ when 'html'
198
+ html = REXML::Element.new('html').add_namespace(JABBER_NS)
199
+ html.add(body.add_namespace(XHTML_NS))
200
+ html
201
+ else
202
+ body
203
+ end
204
+ end
205
+
206
+ def handle_command(body, from, execute = true)
207
+ case body
208
+ when /\Ahe?(?:lp)?\z/i
209
+ deliver(from, <<-HELP) if execute
210
+ h[e[lp]] -- Print this help
211
+
212
+ de[bug] -- Print debug mode
213
+ de[bug] on|off -- Turn debug mode on/off
214
+
215
+ bl[ock] #ID -- Block ID
216
+ fa[v[orite]] #ID -- Create favorite #ID
217
+
218
+ re[ply] #ID: ... -- Reply to ID
219
+ le[n[gth]] ... -- Determine length
220
+ ... -- Update status
221
+
222
+ (Note: Message body must be shorter than #{MAX_LENGTH} characters)
223
+ HELP
224
+ when /\Ade(?:bug)?(?:\s+(on|off))?\z/i
225
+ if execute
226
+ flag = $1.downcase if $1
227
+
228
+ case flag
229
+ when 'on'
230
+ @debug = true
231
+ when 'off'
232
+ @debug = false
233
+ end
234
+
235
+ deliver(from, "DEBUG = #{debug ? 'on' : 'off'}")
236
+ end
237
+ when /\Abl(?:ock)?\s+#?(\d+)\z/i
238
+ twitter.block($1) if execute && !debug
239
+ when /\Afav?(?:orite)?\s+#?(\d+)\z/i
240
+ twitter.favorite_create($1) if execute && !debug
241
+ else
242
+ options = {}
243
+
244
+ if execute && body.sub!(/\Alen?(?:gth)?\s+/i, '')
245
+ if body = handle_command(body, from, false)
246
+ length = body.length
247
+ hint = length <= MAX_LENGTH ? 'OK' : 'TOO LONG'
248
+
249
+ deliver(from, "#{length} [#{hint}]: #{body}")
250
+ end
251
+
252
+ return
253
+ end
254
+
255
+ if body.sub!(/\Are(?:ply)?\s+#?(\d+):?\s+/i, '')
256
+ options[:in_reply_to_status_id] = $1
257
+ end
258
+
259
+ return body unless execute
260
+
261
+ if body.length <= MAX_LENGTH
262
+ update(body, options)
263
+ else
264
+ deliver(from, "MSG TOO LONG (> #{MAX_LENGTH}): #{body}")
265
+ end
266
+ end
267
+ end
268
+
269
+ def deliver(recipient, msg)
270
+ if debug
271
+ logj "#{recipient}: #{msg}", true
272
+ return
273
+ end
274
+
275
+ jabber.deliver(recipient, msg)
276
+ end
277
+
278
+ def update(msg, options = {})
279
+ if debug
280
+ logt "#{msg} (#{options.inspect})", true
281
+ return
282
+ end
283
+
284
+ twitter.update(msg, options)
285
+ end
286
+
287
+ def log(msg, verbose = verbose)
288
+ warn "[#{id}] #{msg}" if verbose
289
+ end
290
+
291
+ def logt(msg, verbose = verbose)
292
+ log("TWITTER #{msg}", verbose)
293
+ end
294
+
295
+ def logj(msg, verbose = verbose)
296
+ log("JABBER #{msg}", verbose)
131
297
  end
132
298
 
133
299
  end
@@ -0,0 +1,7 @@
1
+ <%= user.name %>
2
+ (<a href="http://twitter.com/<%= user.screen_name %>"><%= user.screen_name %></a>)
3
+ [<%= tweet.created_at %>]:
4
+ <br /><br />
5
+ <img src="<%= user.profile_image_url %>" alt="[<%= user.screen_name %>]" />
6
+ <%= tweet.text %>
7
+ | #<%= tweet.id %>
@@ -0,0 +1,3 @@
1
+ <%= user.name %> (<%= user.screen_name %>) [<%= tweet.created_at %>]:
2
+
3
+ <%= tweet.text %> | #<%= tweet.id %>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twitter2jabber
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jens Wille
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-07-29 00:00:00 +02:00
12
+ date: 2009-08-02 00:00:00 +02:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -62,6 +62,8 @@ files:
62
62
  - ChangeLog
63
63
  - TODO
64
64
  - sample/config.yaml
65
+ - sample/templates/tweet.html
66
+ - sample/templates/tweet.txt
65
67
  has_rdoc: true
66
68
  homepage: http://twitter2jabber.rubyforge.org/
67
69
  licenses: []