twitter2jabber 0.0.2 → 0.0.3

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