twitter2jabber 0.7.0 → 0.8.0

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.
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.