twitter2jabber 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c21ae167fd1fabf00e9087a1e58786ad0e73e9c2
4
- data.tar.gz: f178c4940a6e56a74b1b40b70a147d9bde19cfe6
3
+ metadata.gz: d434391b12803f08dec2790028dccd36ec5db99c
4
+ data.tar.gz: 7ab9bac4e82d981f0281f16990ef2fc7ea6d52fb
5
5
  SHA512:
6
- metadata.gz: 1bac7ed5bd3aac7d566d18c4f779fd64bfaadc2adfa096d37a21e68f49e042ba4784fb64d4aa1729c0d07aa5bd6a7212e57cfb97ab62be00445740ec4a229ca5
7
- data.tar.gz: b78d7192b69c42045e2df687160ec222faea9e979dcd186bf226b2b79a7d4af13bad1678d6c60003a60b712451cd09d28ca345c46175db266a62c8c7e9c1ba68
6
+ metadata.gz: f6909849d8bd8705a47aeb84c940841344e4a41f852d349108e0c8c7edd9ae44b78179ec6ccea5cfdbbe9f37f93dd252bcf34cbd549dab9efe42587b31daf1f2
7
+ data.tar.gz: ed809ca1fce70a4479d196232d506e4585dd77197e66f6b27b781177ccd6be0abba952c4ecee7b11facf8727a75ed0c719af8070ccbe77711ed19520555227f5
data/ChangeLog CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  = Revision history for twitter2jabber
4
4
 
5
+ == 0.8.0 [2014-04-15]
6
+
7
+ * Dropped "write" functionality. Only forwards Twitter messages to Jabber.
8
+ * Dropped Ruby 1.8 support.
9
+ * Reduced external dependencies.
10
+ * Cut down command line options.
11
+
5
12
  == 0.0.1 [2009-07-29]
6
13
 
7
14
  * Birthday :-)
data/README CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  == VERSION
4
4
 
5
- This documentation refers to twitter2jabber version 0.7.0
5
+ This documentation refers to twitter2jabber version 0.8.0
6
6
 
7
7
 
8
8
  == DESCRIPTION
9
9
 
10
- Twitter-to-Jabber gateway.
10
+ Allows you to read Twitter[https://twitter.com] streams with your
11
+ favourite {Jabber client}[http://xmpp.org/xmpp-software/clients/].
11
12
 
12
13
  Based on a script[http://da.nmilne.com/?p=353] by dkam. See also his
13
14
  Jitter[http://github.com/dkam/jitter] project.
@@ -15,10 +16,9 @@ Jitter[http://github.com/dkam/jitter] project.
15
16
 
16
17
  == LINKS
17
18
 
18
- <b></b>
19
- Documentation:: http://blackwinter.github.com/twitter2jabber
20
- Source code:: http://github.com/blackwinter/twitter2jabber
21
- RubyGem:: http://rubygems.org/gems/twitter2jabber
19
+ Documentation:: https://blackwinter.github.io/twitter2jabber/
20
+ Source code:: https://github.com/blackwinter/twitter2jabber
21
+ RubyGem:: https://rubygems.org/gems/twitter2jabber
22
22
 
23
23
 
24
24
  == AUTHORS
@@ -28,7 +28,7 @@ RubyGem:: http://rubygems.org/gems/twitter2jabber
28
28
 
29
29
  == LICENSE AND COPYRIGHT
30
30
 
31
- Copyright (C) 2009-2013 Jens Wille
31
+ Copyright (C) 2009-2014 Jens Wille
32
32
 
33
33
  twitter2jabber is free software: you can redistribute it and/or modify it
34
34
  under the terms of the GNU Affero General Public License as published by
data/Rakefile CHANGED
@@ -4,18 +4,18 @@ begin
4
4
  require 'hen'
5
5
 
6
6
  Hen.lay! {{
7
- :gem => {
8
- :name => %q{twitter2jabber},
9
- :version => Twitter2Jabber::VERSION,
10
- :summary => %q{Twitter-to-Jabber gateway.},
11
- :author => %q{Jens Wille},
12
- :email => %q{jens.wille@gmail.com},
13
- :license => %q{AGPL},
14
- :homepage => :blackwinter,
15
- :dependencies => %w[
16
- elif highline longurl ruby-nuggets
17
- shorturl xmpp4r-simple-19
18
- ] << ['twitter', '>= 5.0']
7
+ gem: {
8
+ name: %q{twitter2jabber},
9
+ version: Twitter2Jabber::VERSION,
10
+ summary: %q{Twitter-to-Jabber gateway.},
11
+ description: %q{Read Twitter streams through Jabber.},
12
+ author: %q{Jens Wille},
13
+ email: %q{jens.wille@gmail.com},
14
+ license: %q{AGPL-3.0},
15
+ homepage: :blackwinter,
16
+ dependencies: %w[cyclops longurl xmpp4r] << ['twitter', '>= 5.0'],
17
+
18
+ required_ruby_version: '>= 1.9.3'
19
19
  }
20
20
  }}
21
21
  rescue LoadError => err
data/TODO CHANGED
@@ -1,12 +1,6 @@
1
1
  - include_entities=true (http://dev.twitter.com/pages/tweet_entities)
2
2
  - include_rts=true (?)
3
- - daemon control script (twitter2jabber-ctl)
4
- - threading? (decouple Twitter and Jabber event loops)
5
3
  - use streaming API? (http://apiwiki.twitter.com/Streaming-API-Documentation)
6
4
  - persistent cache? (friends_timeline: since/since_id?)
7
- - better interrupt handling (in loop mode)
8
- - daemonize after asking for credentials (in loop mode)
9
- - additional commands? (full twitter API?)
10
- - refactor command handling!
11
5
  - SPECS!!!
12
6
  - DOCS!!
data/bin/twitter2jabber CHANGED
@@ -5,7 +5,7 @@
5
5
  # #
6
6
  # twitter2jabber - Twitter-to-Jabber gateway. #
7
7
  # #
8
- # Copyright (C) 2009-2013 Jens Wille #
8
+ # Copyright (C) 2009-2014 Jens Wille #
9
9
  # #
10
10
  # Authors: #
11
11
  # Jens Wille <jens.wille@gmail.com> #
@@ -26,210 +26,5 @@
26
26
  ###############################################################################
27
27
  #++
28
28
 
29
- $KCODE = 'u' if RUBY_VERSION < '1.9'
30
-
31
- require 'optparse'
32
- require 'yaml'
33
-
34
- require 'rubygems'
35
- require 'highline/import'
36
-
37
- $: << File.join(File.dirname(__FILE__), '..', 'lib')
38
-
39
- require 'twitter2jabber'
40
-
41
- USAGE = "Usage: #{$0} [-h|--help] [options]"
42
-
43
- def ask(prompt)
44
- super
45
- rescue Interrupt
46
- puts
47
- exit
48
- end
49
-
50
- options = {
51
- :config => 'config.yaml',
52
- :consumer_token => nil,
53
- :access_token => nil,
54
- :jid => nil,
55
- :recipients => [],
56
- :formats => nil,
57
- :template_dir => nil,
58
- :loop => false,
59
- :pause => nil,
60
- :last => nil,
61
- :wrap => nil,
62
- :log => STDERR,
63
- :verbose => false,
64
- :debug => false
65
- }
66
-
67
- OptionParser.new { |opts|
68
- opts.banner = USAGE
69
-
70
- opts.separator ' '
71
- opts.separator 'Options:'
72
-
73
- opts.on('-c', '--config YAML', "Path to config file [Default: #{options[:config]}]") { |c|
74
- options[:config] = c
75
- }
76
-
77
- opts.separator ' '
78
-
79
- opts.on('-C', '--consumer-token TOKEN', 'Twitter consumer token') { |t|
80
- options[:consumer_token] = t
81
- }
82
-
83
- opts.on('-A', '--access-token TOKEN', 'Twitter access token') { |t|
84
- options[:access_token] = t
85
- }
86
-
87
- opts.on('-j', '--jid ID', 'Jabber ID') { |j|
88
- options[:jid] = j
89
- }
90
-
91
- opts.separator ' '
92
-
93
- opts.on('-r', '--recipients LIST', 'Comma-separated list of Jabber IDs') { |r|
94
- options[:recipients] += r.split(',')
95
- }
96
-
97
- opts.on('-R', '--recipients-from-file FILE', 'File with one Jabber ID per line') { |r|
98
- options[:recipients] += File.readlines(r).map { |l| l.chomp }
99
- }
100
-
101
- opts.separator ' '
102
-
103
- opts.on('-f', '--formats LIST', "Comma-separated list of formats [Default: #{Twitter2Jabber::DEFAULT_FORMATS.join(',')}]") { |f|
104
- options[:formats] = f.split(',')
105
- }
106
-
107
- opts.on('-t', '--template-dir PATH', 'Path to custom template directory', "[Default: #{Twitter2Jabber::DEFAULT_TEMPLATES}]") { |t|
108
- abort "Template directory not found: #{t}" unless File.directory?(t)
109
- options[:template_dir] = t
110
- }
111
-
112
- opts.separator ' '
113
-
114
- opts.on('-l', '--loop', 'Run in a loop') {
115
- options[:loop] = true
116
- }
117
-
118
- opts.on('-p', '--pause SECONDS', Integer, 'Number of seconds to pause between loops') { |p|
119
- options[:pause] = p
120
- }
121
-
122
- opts.separator ' '
123
-
124
- opts.on('-S', '--since-id ID', 'Return tweets with status IDs greater than ID') { |i|
125
- options[:last] = i
126
- }
127
-
128
- opts.separator ' '
129
-
130
- opts.on('-L', '--loop-wrap [LOG]', "Convenience switch: implies '--loop', '--verbose', and", "'--since-id'; kills existing process and logs output to LOG") { |l|
131
- options[:wrap] = true
132
- options[:log] = l && File.open(l, 'a')
133
- }
134
-
135
- opts.separator ' '
136
- opts.separator 'Generic options:'
137
-
138
- opts.on('-v', '--verbose', 'Be verbose') {
139
- options[:verbose] = true
140
- }
141
-
142
- opts.on('-D', '--debug', "Print debug output and don't send or update anything") {
143
- options[:debug] = true
144
- }
145
-
146
- opts.on('-h', '--help', 'Print this help message and exit') {
147
- puts opts
148
- exit
149
- }
150
-
151
- opts.on('--version', 'Print program version and exit') {
152
- puts "#{File.basename($0)} v#{Twitter2Jabber::VERSION}"
153
- exit
154
- }
155
- }.parse!
156
-
157
- config = begin
158
- YAML.load_file(options[:config])
159
- rescue Errno::ENOENT
160
- {}
161
- end
162
-
163
- # twitter config
164
- config[:twitter] ||= {}
165
- config[:twitter][:consumer_token] = options[:consumer_token] if options[:consumer_token]
166
- config[:twitter][:consumer_token] ||= ask('Twitter consumer token: ')
167
- config[:twitter][:consumer_secret] ||= ask("Consumer secret for Twitter application #{config[:twitter][:consumer_token]}: ") { |q| q.echo = false }
168
- config[:twitter][:access_token] = options[:access_token] if options[:access_token]
169
- config[:twitter][:access_token] ||= ask('Twitter access token: ')
170
- config[:twitter][:access_secret] ||= ask("Access secret for Twitter user #{config[:twitter][:access_token]}: ") { |q| q.echo = false }
171
-
172
- # jabber config
173
- config[:jabber] ||= {}
174
- config[:jabber][:user] = options[:jid] if options[:jid]
175
- config[:jabber][:user] ||= ask('Jabber ID: ')
176
- config[:jabber][:pass] ||= ask("Password for Jabber ID #{config[:jabber][:user]}: ") { |q| q.echo = false }
177
-
178
- recipients = options[:recipients] + (config.delete(:recipients) || [])
179
- recipients.uniq!
180
-
181
- [:formats, :template_dir, :log, :verbose, :debug].each { |key|
182
- config[key] = options[key] if options[key]
183
- }
184
-
185
- begin
186
- if options[:wrap]
187
- options[:loop] = true
188
- config[:verbose] = true
189
-
190
- case log = config[:log]
191
- when String
192
- config[:log] = File.open(log, 'a')
193
- when File
194
- log = log.path
195
- when IO
196
- abort "Invalid log file: #{log.tty? ? "FILENO=#{log.fileno}" : log}"
197
- else
198
- abort 'Log file missing!'
199
- end
200
-
201
- if File.readable?(log)
202
- require 'elif'
203
-
204
- pid = nil
205
-
206
- Elif.foreach(log) { |line|
207
- case line.chomp
208
- when /\A(\d+)\z/
209
- pid ||= $1.to_i
210
- break if options[:last]
211
- when / TWITTER (\d+)\z/
212
- options[:last] ||= $1.to_i
213
- break if pid
214
- end
215
- }
216
-
217
- begin
218
- Process.kill(:INT, pid)
219
- sleep 1
220
- rescue Errno::ESRCH
221
- end if pid
222
- end
223
- end
224
-
225
- twitter2jabber = Twitter2Jabber.new(config)
226
-
227
- if options[:loop]
228
- twitter2jabber.send(:log_, Process.pid)
229
- twitter2jabber.loop(recipients, options[:pause] || config[:pause], options[:last])
230
- else
231
- twitter2jabber.run(recipients, options[:last])
232
- end
233
- rescue RuntimeError => err
234
- abort err
235
- end
29
+ require 'twitter2jabber/cli'
30
+ Twitter2Jabber::CLI.execute
data/example/config.yaml CHANGED
@@ -1,4 +1,6 @@
1
1
  ---
2
+ #:log: ~/twitter2jabber.log
3
+
2
4
  :twitter:
3
5
  :consumer_token: c5ff16bd638c9acc6fb00197301008c5eb561f46
4
6
  :consumer_secret: ae2ad9454f3af7fcb18c83969f99b20a788eddd1
@@ -6,12 +8,8 @@
6
8
  :access_secret: 06c76739e3f4875a27a9e3ce36999e1af916a0e0
7
9
 
8
10
  :jabber:
9
- :user: foo
10
- :pass: foo
11
-
12
- :recipients:
13
- - one
14
- - two
15
- - three
16
-
17
- :pause: 123
11
+ #:format: txt
12
+ #:templates: ~/twitter2jabber
13
+ :recipient: joe@example.org
14
+ :username: foo@example.com
15
+ :password: bar
@@ -3,7 +3,7 @@
3
3
  # #
4
4
  # twitter2jabber - Twitter-to-Jabber gateway. #
5
5
  # #
6
- # Copyright (C) 2009-2013 Jens Wille #
6
+ # Copyright (C) 2009-2014 Jens Wille #
7
7
  # #
8
8
  # Authors: #
9
9
  # Jens Wille <jens.wille@gmail.com> #
@@ -24,404 +24,65 @@
24
24
  ###############################################################################
25
25
  #++
26
26
 
27
- require 'erb'
28
-
29
- require 'rubygems'
30
- require 'twitter'
31
- require 'xmpp4r-simple'
32
- require 'shorturl'
33
- require 'longurl'
34
- require 'nuggets/array/runiq'
35
-
36
- require 'twitter2jabber/version'
37
-
38
27
  class Twitter2Jabber
39
28
 
40
- MAX_LENGTH = 140
41
-
42
- DEFAULT_PAUSE = 60
43
-
44
- DEFAULT_FORMATS = %w[txt]
45
-
46
- DEFAULT_TEMPLATES = File.expand_path(File.join(File.dirname(__FILE__), %w[.. example templates]))
47
-
48
- JABBER_NS = 'http://jabber.org/protocol/xhtml-im'
49
- XHTML_NS = 'http://www.w3.org/1999/xhtml'
50
-
51
- def self.loop(options, recipients = [], pause = nil, last = nil, &block)
52
- new(options).loop(recipients, pause, last, &block)
53
- end
54
-
55
- def self.run(options, recipients = [], last = nil, &block)
56
- new(options).run(recipients, last, &block)
57
- end
58
-
59
- attr_reader :id, :verbose, :debug, :log, :twitter, :jabber, :filter, :formats, :templates, :_erb
60
-
61
- def initialize(options, &block)
62
- [:twitter, :jabber].each { |client|
63
- raise ArgumentError, "#{client} config missing" unless options[client].is_a?(Hash)
64
- }
65
-
66
- @id = "#{options[:twitter][:consumer_token]} -> #{options[:jabber][:user]}"
67
-
68
- @verbose = options[:verbose]
69
- @debug = options[:debug]
70
- @log = options[:log]
71
-
72
- log.sync = true
73
- logm 'HAI!'
74
-
75
- twitter_connect(options[:twitter])
76
- jabber_connect(options[:jabber])
77
-
78
- @filter = options[:filter] || block
79
- @formats = options[:formats] || DEFAULT_FORMATS
80
-
81
- @templates = Dir[
82
- File.join(options[:template_dir] || DEFAULT_TEMPLATES, 'tweet.*')
83
- ].inject({}) { |hash, template|
84
- hash.update(File.extname(template).sub(/\A\./, '') => File.read(template))
85
- }
86
-
87
- @_erb = Hash.new { |hash, format|
88
- template = templates[format]
89
- hash[format] = template && ERB.new(template)
90
- }
91
- end
92
-
93
- def run(recipients = [], last = nil, flag = true, &block)
94
- last = deliver_tweets(recipients, last, &block) if flag
95
- post_messages(recipients)
96
-
97
- last
98
- end
99
-
100
- def loop(recipients = [], pause = nil, last = nil, &block)
101
- pause ||= DEFAULT_PAUSE
102
-
103
- i = 1
29
+ class << self
104
30
 
105
- trap(:INT) {
106
- logm 'SIGINT received, shutting down...'
107
- i = -1
108
- }
109
-
110
- while i > 0
111
- last = run(recipients, last, i % pause == 1, &block)
112
-
113
- sleep 1
114
-
115
- i += 1
31
+ def run(options, since_id = nil)
32
+ new(options).connect.deliver_tweets(since_id)
116
33
  end
117
34
 
118
- logm 'KTHXBYE!'
119
-
120
- last
121
- end
122
-
123
- def deliver_tweets(recipients, last = nil, &block)
124
- get_tweets(last).each { |tweet|
125
- logt last = tweet.id
126
-
127
- # apply filters
128
- next if filter && !filter[tweet]
129
- next if block && !block[tweet]
130
-
131
- msg = format_tweet(tweet)
132
-
133
- recipients.each { |recipient|
134
- deliver(recipient, msg)
135
- }
136
-
137
- sleep 1
138
- }
139
-
140
- last
141
- end
142
-
143
- def post_messages(recipients = [])
144
- allowed = %r{\A(?:#{recipients.map { |r| Regexp.escape(r) }.join('|')})\z}i
145
-
146
- jabber.received_messages { |msg|
147
- next unless msg.type == :chat
148
- next unless msg.from.bare.to_s =~ allowed
149
-
150
- logj msg.id
151
-
152
- handle_command(msg.body, msg.from)
153
- }
154
- end
155
-
156
- private
157
-
158
- def twitter_connect(options = @twitter_options)
159
- @twitter_options = options
160
-
161
- @twitter = Twitter::REST::Client.new(
162
- :consumer_key => options[:consumer_token],
163
- :consumer_secret => options[:consumer_secret],
164
- :access_token => options[:access_token],
165
- :access_token_secret => options[:access_secret]
166
- )
167
-
168
- @twitter.verify_credentials
169
-
170
- logt 'connected'
171
-
172
- @twitter
173
- rescue Twitter::Error => err
174
- raise "Can't connect to Twitter with ID '#{options[:consumer_token]}': #{err}"
175
- end
176
-
177
- def jabber_connect(options = @jabber_options)
178
- @jabber_options = options
179
-
180
- @jabber = Jabber::Simple.new(options[:user], options[:pass])
181
-
182
- logj 'connected'
183
-
184
- @jabber
185
- rescue Jabber::JabberError, Errno::ETIMEDOUT => err
186
- raise "Can't connect to Jabber with JID '#{options[:user]}': #{err}"
187
- end
188
-
189
- def get_tweets(last = nil)
190
- options = {}
191
- options[:since_id] = last if last
192
-
193
- tweets = twitter.home_timeline(options)
194
- return [] unless tweets.is_a?(Array)
195
-
196
- tweets.sort_by { |tweet| tweet.created_at }
197
- rescue Twitter::Error, Timeout::Error
198
- []
199
- rescue => err
200
- warn "#{err} (#{err.class})"
201
- []
202
- end
203
-
204
- def format_tweet(tweet)
205
- user = tweet.user
206
-
207
- msg = Jabber::Message.new.set_type(:chat)
208
-
209
- formats.each { |format|
210
- if erb = _erb[format]
211
- msg.add_element(format_element(format, erb.result(binding)))
212
- end
213
- }
214
-
215
- msg
216
- end
217
-
218
- # cf. <http://devblog.famundo.com/articles/2006/10/18/ruby-and-xmpp-jabber-part-3-adding-html-to-the-messages>
219
- def format_element(format, text)
220
- text = process_message(text)
221
- body = REXML::Element.new('body')
222
-
223
- case format
224
- when 'html'
225
- REXML::Text.new(process_html(text), false, body, true, nil, /.^/)
226
-
227
- html = REXML::Element.new('html').add_namespace(JABBER_NS)
228
- html.add(body.add_namespace(XHTML_NS))
229
- html
230
- else
231
- REXML::Text.new(process_text(text), true, body, true, nil, /.^/)
232
- body
35
+ def client(client)
36
+ const_get("#{client.to_s.capitalize}Client")
233
37
  end
234
- end
235
-
236
- def process_message(text)
237
- text.gsub(%r{https?://\S+}) { |match| LongURL.expand(match) rescue match }
238
- end
239
-
240
- def process_html(text)
241
- text.gsub(/(?:\A|\W)@(\w+)/, '@<a href="https://twitter.com/\1">\1</a>').
242
- gsub(/(?:\A|\W)#(\w+)/, '<a href="https://search.twitter.com/search?q=%23\1">#\1</a>')
243
- end
244
38
 
245
- def process_text(text)
246
- text
247
39
  end
248
40
 
249
- def handle_command(body, from, execute = true)
250
- case body
251
- when /\A(?:he?(?:lp)?|\?)\z/i
252
- deliver(from, <<-HELP) if execute
253
- h[e[lp]]|? -- Print this help
254
-
255
- de[bug] -- Print debug mode
256
- de[bug] on|off -- Turn debug mode on/off
257
-
258
- bl[ock] @USER -- Block USER
259
- fa[v[orite]] #ID -- Add ID to favorites
260
-
261
- bm|bookmark[s] -- List bookmarks
262
- bm|bookmark #ID -- Bookmark ID
263
- bm|bookmark -#ID -- Remove ID from bookmarks
41
+ def initialize(options)
42
+ @twitter = initialize_client(:twitter, options)
43
+ @jabber = initialize_client(:jabber, options)
264
44
 
265
- rt|retweet #ID [!] [STATUS] -- Retweet ID
266
- re[ply] #ID [!] STATUS -- Reply to ID
267
- d[m]|direct[_message] @USER [!] STATUS -- Send direct message to USER
45
+ if @debug = options[:debug] or options[:verbose]
46
+ @spec = "#{twitter.spec} -> #{jabber.spec}"
268
47
 
269
- le[n[gth]] STATUS -- Determine length
270
- [!] STATUS -- Update status
48
+ @log = options[:log] || $stderr
49
+ @log.sync = true
271
50
 
272
- (Note: STATUS must be under #{MAX_LENGTH} characters; force send with '!'.)
273
- HELP
274
- when /\Ade(?:bug)?(?:\s+(on|off))?\z/i
275
- if execute
276
- flag = $1.downcase if $1
277
-
278
- case flag
279
- when 'on'
280
- @debug = true
281
- when 'off'
282
- @debug = false
283
- end
284
-
285
- deliver(from, "DEBUG = #{debug ? 'on' : 'off'}")
286
- end
287
- when /\Abl(?:ock)?\s+[@#]?(\w+)\z/i
288
- twitter.block($1) if execute && !debug
289
- when /\Afav?(?:orite)?\s+#?(\d+)\z/i
290
- twitter.favorite_create($1) if execute && !debug
291
- when /\A(?:bm|bookmarks?)\z/i
292
- deliver(from, bookmarks.map { |bm|
293
- st = begin; twitter.status(bm); rescue Twitter::Error::NotFound; end
294
- st = "@#{st.user.screen_name}: #{st.text[0, 10]}..." if st.is_a?(Twitter::Tweet)
295
- "#{bm} - #{st || '???'}"
296
- }.join("\n")) if execute && !debug
297
- when /\A(?:bm|bookmark)\s+(\d+)\z/i
298
- if execute && !debug
299
- bookmarks << $1
300
- bookmarks.runiq!
301
- end
302
- when /\A(?:bm|bookmark)\s+-(\d+)\z/i
303
- bookmarks.delete($1) if execute && !debug
304
- else
305
- command, options = nil, {}
306
-
307
- if execute && body.sub!(/\Alen?(?:gth)?\s+/i, '')
308
- if body = handle_command(body, from, false)
309
- length = body.length
310
- hint = length <= MAX_LENGTH ? 'OK' : 'TOO LONG'
311
-
312
- deliver(from, "#{length} [#{hint}]: #{body}")
313
- end
314
-
315
- return
316
- end
317
-
318
- begin
319
- if body.sub!(/\A(?:rt|retweet)\s+#?(\d+)(:?)(?:\s+|\z)/i, '')
320
- id, colon = $1, $2
321
-
322
- tweet = twitter.status(id)
323
-
324
- if body.empty?
325
- options[:id] = id
326
- command = :rt
327
- else
328
- body << " RT @#{tweet.user.screen_name}#{colon} #{tweet.text}"
329
- end
330
- elsif body.sub!(/\Are(?:ply)?\s+#?(\d+)(:?)\s+/i, '')
331
- id, colon = $1, $2
332
-
333
- tweet = twitter.status(id)
334
-
335
- body.insert(0, ' ') unless body.empty?
336
- body.insert(0, "@#{tweet.user.screen_name}#{colon}")
337
-
338
- options[:in_reply_to_status_id] = id
339
- end
340
- rescue Twitter::Error::NotFound
341
- deliver(from, "TWEET NOT FOUND: #{id}")
342
- return
343
- end
344
-
345
- if body.sub!(/\A(?:dm?|direct(?:_message)?)\s+[@#]?(\w+):?\s+/i, '')
346
- options[:user] = $1
347
- command = :dm
348
- end
349
-
350
- if body.sub!(/\A!(?:\s+|\z)/, '')
351
- force = true
352
- end
353
-
354
- body.gsub!(/https?:\/\/\S+/) { |match|
355
- short = ShortURL.shorten(match, :tinyurl)
356
- short && short.length < match.length ? short : match
357
- }
358
-
359
- return body unless execute
360
-
361
- if force || body.length <= MAX_LENGTH
362
- case command
363
- when :rt
364
- if debug
365
- logt "RT: #{options.inspect}", true
366
- else
367
- twitter.retweet(options[:id])
368
- end
369
- when :dm
370
- if debug
371
- logt "DM: #{body} (#{options.inspect})", true
372
- else
373
- twitter.direct_message_create(options[:user], body)
374
- end
375
- else
376
- update(body, options)
377
- end
378
-
379
- deliver(from, "MSG SENT: #{body.inspect}, #{options.inspect}")
380
- else
381
- deliver(from, "MSG TOO LONG (> #{MAX_LENGTH}): #{body.inspect}")
382
- end
51
+ require 'time'
52
+ else
53
+ define_singleton_method(:log) { |_| }
383
54
  end
384
55
  end
385
56
 
386
- def deliver(recipient, msg)
387
- if debug
388
- logj "#{recipient}: #{msg}", true
389
- return
390
- end
391
-
392
- jabber.deliver(recipient, msg)
393
- rescue => err
394
- warn "#{err} (#{err.class})"
395
- jabber_connect and retry if err.is_a?(Jabber::ServerDisconnected)
396
- end
57
+ def connect
58
+ log 'Connecting...'
397
59
 
398
- def update(msg, options = {})
399
- if debug
400
- logt "#{msg} (#{options.inspect})", true
401
- return
402
- end
60
+ twitter.connect
61
+ jabber.connect
403
62
 
404
- twitter.update(msg, options)
63
+ self
405
64
  end
406
65
 
407
- def log_(msg, verbose = verbose)
408
- log.puts msg if verbose
409
- end
66
+ attr_reader :twitter, :jabber, :debug
410
67
 
411
- def logm(msg, verbose = verbose)
412
- log_("#{Time.now} [#{id}] #{msg}", verbose)
68
+ def deliver_tweets(since_id = nil)
69
+ twitter.tweets(since_id) { |tweet| jabber.deliver(tweet) }
413
70
  end
414
71
 
415
- def logt(msg, verbose = verbose)
416
- logm("TWITTER #{msg}", verbose)
72
+ def log(msg)
73
+ @log.puts "#{Time.now.xmlschema} [#{@spec}] #{msg}"
417
74
  end
418
75
 
419
- def logj(msg, verbose = verbose)
420
- logm("JABBER #{msg}", verbose)
421
- end
76
+ private
422
77
 
423
- def bookmarks
424
- @bookmarks ||= []
78
+ def initialize_client(client, options)
79
+ (config = options[client]).is_a?(Hash) ?
80
+ self.class.client(client).new(self, config) :
81
+ raise(ArgumentError, "#{client} config missing")
425
82
  end
426
83
 
427
84
  end
85
+
86
+ require_relative 'twitter2jabber/version'
87
+ require_relative 'twitter2jabber/twitter'
88
+ require_relative 'twitter2jabber/jabber'
@@ -0,0 +1,100 @@
1
+ #--
2
+ ###############################################################################
3
+ # #
4
+ # twitter2jabber - Twitter-to-Jabber gateway. #
5
+ # #
6
+ # Copyright (C) 2009-2014 Jens Wille #
7
+ # #
8
+ # Authors: #
9
+ # Jens Wille <jens.wille@gmail.com> #
10
+ # #
11
+ # twitter2jabber is free software; you can redistribute it and/or modify it #
12
+ # under the terms of the GNU Affero General Public License as published by #
13
+ # the Free Software Foundation; either version 3 of the License, or (at your #
14
+ # option) any later version. #
15
+ # #
16
+ # twitter2jabber is distributed in the hope that it will be useful, but #
17
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
18
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public #
19
+ # License for more details. #
20
+ # #
21
+ # You should have received a copy of the GNU Affero General Public License #
22
+ # along with twitter2jabber. If not, see <http://www.gnu.org/licenses/>. #
23
+ # #
24
+ ###############################################################################
25
+ #++
26
+
27
+ require 'cyclops'
28
+ require 'twitter2jabber'
29
+
30
+ class Twitter2Jabber
31
+
32
+ class CLI < Cyclops
33
+
34
+ class << self
35
+
36
+ def defaults
37
+ super.merge(
38
+ config: 'config.yaml',
39
+ since_id: nil,
40
+ verbose: false,
41
+ debug: false
42
+ )
43
+ end
44
+
45
+ def extract_since_id(log)
46
+ return unless File.readable?(log)
47
+
48
+ id, re = nil, /\bTWITTER\s+(\d+)\Z/
49
+
50
+ File.foreach(log) { |line|
51
+ id = $1 if line =~ re
52
+ }
53
+
54
+ id.to_i if id
55
+ end
56
+
57
+ end
58
+
59
+ def run(arguments)
60
+ if log = options[:log]
61
+ options[:log] = File.open(log, 'a')
62
+ options[:since_id] ||= self.class.extract_since_id(log)
63
+ end
64
+
65
+ Twitter2Jabber.run(options, options.delete(:since_id))
66
+ end
67
+
68
+ private
69
+
70
+ def parse_options(arguments)
71
+ super
72
+
73
+ options[:log] &&= File.expand_path(options[:log])
74
+
75
+ t = options[:twitter] ||= {}
76
+ t[:consumer_token] ||= ask('Twitter consumer token: ')
77
+ t[:consumer_secret] ||= askpass("Consumer secret for Twitter application #{t[:consumer_token]}: ")
78
+ t[:access_token] ||= ask('Twitter access token: ')
79
+ t[:access_secret] ||= askpass("Access secret for Twitter user #{t[:access_token]}: ")
80
+
81
+ j = options[:jabber] ||= {}
82
+ j[:username] ||= ask('Jabber ID: ')
83
+ j[:password] ||= askpass("Password for Jabber ID #{j[:username]}: ")
84
+ end
85
+
86
+ def opts(opts)
87
+ opts.option(:since_id__ID, Integer, 'Return tweets with status IDs greater than ID')
88
+
89
+ opts.separator
90
+
91
+ opts.option(:log__FILE, 'Path to log file [Default: STDERR]')
92
+ end
93
+
94
+ def debug_message
95
+ "don't send any messages"
96
+ end
97
+
98
+ end
99
+
100
+ end
@@ -0,0 +1,132 @@
1
+ #--
2
+ ###############################################################################
3
+ # #
4
+ # twitter2jabber - Twitter-to-Jabber gateway. #
5
+ # #
6
+ # Copyright (C) 2009-2014 Jens Wille #
7
+ # #
8
+ # Authors: #
9
+ # Jens Wille <jens.wille@gmail.com> #
10
+ # #
11
+ # twitter2jabber is free software; you can redistribute it and/or modify it #
12
+ # under the terms of the GNU Affero General Public License as published by #
13
+ # the Free Software Foundation; either version 3 of the License, or (at your #
14
+ # option) any later version. #
15
+ # #
16
+ # twitter2jabber is distributed in the hope that it will be useful, but #
17
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
18
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public #
19
+ # License for more details. #
20
+ # #
21
+ # You should have received a copy of the GNU Affero General Public License #
22
+ # along with twitter2jabber. If not, see <http://www.gnu.org/licenses/>. #
23
+ # #
24
+ ###############################################################################
25
+ #++
26
+
27
+ require 'erb'
28
+ require 'xmpp4r'
29
+
30
+ class Twitter2Jabber
31
+
32
+ class JabberClient
33
+
34
+ DEFAULT_TEMPLATES = File.expand_path('../../../example/templates', __FILE__)
35
+
36
+ DEFAULT_FORMAT = 'txt'
37
+
38
+ JABBER_NS = 'http://jabber.org/protocol/xhtml-im'
39
+ XHTML_NS = 'http://www.w3.org/1999/xhtml'
40
+
41
+ def initialize(gw, config)
42
+ @gw, @config, @spec = gw, config, config[:username]
43
+
44
+ @format = config.fetch(:format, DEFAULT_FORMAT).to_s
45
+
46
+ if File.readable?(template = File.expand_path(File.join(
47
+ config[:templates] || DEFAULT_TEMPLATES, "tweet.#{@format}")))
48
+
49
+ @erb = ERB.new(File.read(template))
50
+ else
51
+ raise ArgumentError, "format not supported: #{@format}"
52
+ end
53
+ end
54
+
55
+ attr_reader :config, :spec
56
+
57
+ def client
58
+ @client ||= Jabber::Client.new(spec).tap { |client|
59
+ client.connect
60
+ client.auth(config[:password])
61
+ }
62
+ end
63
+
64
+ def connect
65
+ deliver(Jabber::Presence.new(nil, 'Available'), nil)
66
+ log 'connected'
67
+ rescue Jabber::JabberError, Errno::ETIMEDOUT => err
68
+ raise "Can't connect to Jabber with JID '#{spec}': #{err}"
69
+ end
70
+
71
+ def format(tweet)
72
+ user = tweet.user
73
+ text = @erb.result(binding)
74
+
75
+ msg = Jabber::Message.new.set_type(:chat)
76
+ msg.add_element(format_element(text))
77
+ msg
78
+ end
79
+
80
+ def deliver(msg, recipient = config[:recipient])
81
+ msg = format(msg) unless msg.is_a?(Jabber::XMPPStanza)
82
+ msg.to = Jabber::JID.new(recipient).strip if recipient
83
+
84
+ @gw.debug ? log("#{recipient}: #{msg}") : send_msg(msg)
85
+ rescue => err
86
+ warn "#{err} (#{err.class})"
87
+ retry if err.is_a?(Jabber::ServerDisconnected)
88
+ end
89
+
90
+ private
91
+
92
+ # cf. <http://devblog.famundo.com/articles/2006/10/18/ruby-and-xmpp-jabber-part-3-adding-html-to-the-messages>
93
+ def format_element(text)
94
+ twitter = @gw.twitter
95
+
96
+ text, body = twitter.process_message(text), REXML::Element.new('body')
97
+
98
+ if @format == 'html'
99
+ REXML::Text.new(twitter.process_html(text), false, body, true, nil, /.^/)
100
+
101
+ html = REXML::Element.new('html').add_namespace(JABBER_NS)
102
+ html.add(body.add_namespace(XHTML_NS))
103
+ html
104
+ else
105
+ REXML::Text.new(twitter.process_text(text), true, body, true, nil, /.^/)
106
+ body
107
+ end
108
+ end
109
+
110
+ def send_msg(msg, attempts = 0)
111
+ attempts += 1
112
+ client.send(msg)
113
+ rescue Errno::EPIPE, IOError
114
+ raise if attempts > 3
115
+
116
+ begin
117
+ @client.close
118
+ rescue Errno::EPIPE, IOError
119
+ end
120
+
121
+ @client = nil
122
+ sleep 1
123
+ retry
124
+ end
125
+
126
+ def log(msg)
127
+ @gw.log("JABBER #{msg}")
128
+ end
129
+
130
+ end
131
+
132
+ end
@@ -0,0 +1,89 @@
1
+ #--
2
+ ###############################################################################
3
+ # #
4
+ # twitter2jabber - Twitter-to-Jabber gateway. #
5
+ # #
6
+ # Copyright (C) 2009-2014 Jens Wille #
7
+ # #
8
+ # Authors: #
9
+ # Jens Wille <jens.wille@gmail.com> #
10
+ # #
11
+ # twitter2jabber is free software; you can redistribute it and/or modify it #
12
+ # under the terms of the GNU Affero General Public License as published by #
13
+ # the Free Software Foundation; either version 3 of the License, or (at your #
14
+ # option) any later version. #
15
+ # #
16
+ # twitter2jabber is distributed in the hope that it will be useful, but #
17
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
18
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public #
19
+ # License for more details. #
20
+ # #
21
+ # You should have received a copy of the GNU Affero General Public License #
22
+ # along with twitter2jabber. If not, see <http://www.gnu.org/licenses/>. #
23
+ # #
24
+ ###############################################################################
25
+ #++
26
+
27
+ require 'twitter'
28
+ require 'longurl'
29
+
30
+ class Twitter2Jabber
31
+
32
+ class TwitterClient
33
+
34
+ def initialize(gw, config)
35
+ @gw, @config, @client = gw, config, Twitter::REST::Client.new(
36
+ consumer_key: @spec = config[:consumer_token],
37
+ consumer_secret: config[:consumer_secret],
38
+ access_token: config[:access_token],
39
+ access_token_secret: config[:access_secret]
40
+ )
41
+ end
42
+
43
+ attr_reader :config, :spec, :client
44
+
45
+ def connect
46
+ client.verify_credentials
47
+ log 'connected'
48
+ rescue Twitter::Error => err
49
+ raise "Can't connect to Twitter with ID '#{spec}': #{err}"
50
+ end
51
+
52
+ def tweets(since_id = nil)
53
+ tweets = client.home_timeline(since_id: since_id)
54
+ return unless tweets.is_a?(Array)
55
+
56
+ tweets.sort_by { |tweet| tweet.created_at }.each { |tweet|
57
+ log since_id = tweet.id
58
+ yield tweet
59
+ sleep 1
60
+ }
61
+
62
+ since_id
63
+ rescue Twitter::Error, Timeout::Error
64
+ rescue => err
65
+ warn "#{err} (#{err.class})"
66
+ end
67
+
68
+ def process_message(text)
69
+ text.gsub(%r{https?://\S+}) { |match| LongURL.expand(match) rescue match }
70
+ end
71
+
72
+ def process_html(text)
73
+ text.gsub(/(?:\A|\W)@(\w+)/, '@<a href="https://twitter.com/\1">\1</a>')
74
+ .gsub(/(?:\A|\W)#(\w+)/, '<a href="https://search.twitter.com/search?q=%23\1">#\1</a>')
75
+ end
76
+
77
+ def process_text(text)
78
+ text
79
+ end
80
+
81
+ private
82
+
83
+ def log(msg)
84
+ @gw.log("TWITTER #{msg}")
85
+ end
86
+
87
+ end
88
+
89
+ end
@@ -3,7 +3,7 @@ class Twitter2Jabber
3
3
  module Version
4
4
 
5
5
  MAJOR = 0
6
- MINOR = 7
6
+ MINOR = 8
7
7
  TINY = 0
8
8
 
9
9
  class << self
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twitter2jabber
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jens Wille
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-11-19 00:00:00.000000000 Z
11
+ date: 2014-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: elif
14
+ name: cyclops
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: highline
28
+ name: longurl
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: longurl
42
+ name: xmpp4r
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -53,27 +53,27 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: ruby-nuggets
56
+ name: twitter
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: '5.0'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: '5.0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: shorturl
70
+ name: hen
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
- type: :runtime
76
+ type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
@@ -81,34 +81,20 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: xmpp4r-simple-19
84
+ name: rake
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
- type: :runtime
90
+ type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: twitter
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '5.0'
104
- type: :runtime
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '5.0'
111
- description: Twitter-to-Jabber gateway.
97
+ description: Read Twitter streams through Jabber.
112
98
  email: jens.wille@gmail.com
113
99
  executables:
114
100
  - twitter2jabber
@@ -118,29 +104,40 @@ extra_rdoc_files:
118
104
  - COPYING
119
105
  - ChangeLog
120
106
  files:
121
- - lib/twitter2jabber.rb
122
- - lib/twitter2jabber/version.rb
123
- - bin/twitter2jabber
124
107
  - COPYING
125
108
  - ChangeLog
126
109
  - README
127
110
  - Rakefile
128
111
  - TODO
112
+ - bin/twitter2jabber
129
113
  - example/config.yaml
130
114
  - example/templates/tweet.html
131
115
  - example/templates/tweet.txt
116
+ - lib/twitter2jabber.rb
117
+ - lib/twitter2jabber/cli.rb
118
+ - lib/twitter2jabber/jabber.rb
119
+ - lib/twitter2jabber/twitter.rb
120
+ - lib/twitter2jabber/version.rb
132
121
  homepage: http://github.com/blackwinter/twitter2jabber
133
122
  licenses:
134
- - AGPL
123
+ - AGPL-3.0
135
124
  metadata: {}
136
- post_install_message:
125
+ post_install_message: |2+
126
+
127
+ twitter2jabber-0.8.0 [2014-04-15]:
128
+
129
+ * Dropped "write" functionality. Only forwards Twitter messages to Jabber.
130
+ * Dropped Ruby 1.8 support.
131
+ * Reduced external dependencies.
132
+ * Cut down command line options.
133
+
137
134
  rdoc_options:
135
+ - "--title"
136
+ - twitter2jabber Application documentation (v0.8.0)
138
137
  - "--charset"
139
138
  - UTF-8
140
139
  - "--line-numbers"
141
140
  - "--all"
142
- - "--title"
143
- - twitter2jabber Application documentation (v0.7.0)
144
141
  - "--main"
145
142
  - README
146
143
  require_paths:
@@ -149,7 +146,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
149
146
  requirements:
150
147
  - - ">="
151
148
  - !ruby/object:Gem::Version
152
- version: '0'
149
+ version: 1.9.3
153
150
  required_rubygems_version: !ruby/object:Gem::Requirement
154
151
  requirements:
155
152
  - - ">="
@@ -157,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
154
  version: '0'
158
155
  requirements: []
159
156
  rubyforge_project:
160
- rubygems_version: 2.1.11
157
+ rubygems_version: 2.2.2
161
158
  signing_key:
162
159
  specification_version: 4
163
160
  summary: Twitter-to-Jabber gateway.