rbitter 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +65 -0
- data/Rakefile +8 -0
- data/XMLRPC.md +19 -0
- data/bin/rbitter +6 -0
- data/config.json.example +30 -0
- data/lib/rbitter/arcserver.rb +97 -0
- data/lib/rbitter/console.rb +61 -0
- data/lib/rbitter/default/config_json.rb +37 -0
- data/lib/rbitter/dlthread.rb +66 -0
- data/lib/rbitter/env.rb +59 -0
- data/lib/rbitter/libtwitter_connection_override.rb +46 -0
- data/lib/rbitter/records.rb +109 -0
- data/lib/rbitter/records_migrate/.keep +0 -0
- data/lib/rbitter/records_migrate/20150327_add_index.rb +8 -0
- data/lib/rbitter/streaming.rb +76 -0
- data/lib/rbitter/version.rb +3 -0
- data/lib/rbitter/xmlrpc.rb +4 -0
- data/lib/rbitter/xmlrpcd/base.rb +25 -0
- data/lib/rbitter/xmlrpcd/rpchandles.rb +12 -0
- data/lib/rbitter/xmlrpcd/xmlrpc_auth_server.rb +84 -0
- data/lib/rbitter/xmlrpcd/xmlrpcd.rb +63 -0
- data/lib/rbitter.rb +92 -0
- data/rbitter.gemspec +42 -0
- data/spec/config/.keep +0 -0
- data/spec/config/default.json +33 -0
- data/spec/rbitter/arcserver_spec.rb +9 -0
- data/spec/rbitter/console_spec.rb +9 -0
- data/spec/rbitter/default/config_json_spec.rb +3 -0
- data/spec/rbitter/dlthread_spec.rb +9 -0
- data/spec/rbitter/env_spec.rb +56 -0
- data/spec/rbitter/libtwitter_connection_override_spec.rb +8 -0
- data/spec/rbitter/records_spec.rb +13 -0
- data/spec/rbitter/streaming_spec.rb +9 -0
- data/spec/rbitter/version_spec.rb +8 -0
- data/spec/rbitter/xmlrpc_spec.rb +8 -0
- data/spec/rbitter/xmlrpcd/base_spec.rb +29 -0
- data/spec/rbitter/xmlrpcd/rpchandles_spec.rb +10 -0
- data/spec/rbitter/xmlrpcd/xmlrpc_auth_server_spec.rb +8 -0
- data/spec/rbitter/xmlrpcd/xmlrpcd_spec.rb +9 -0
- data/spec/rbitter_spec.rb +46 -0
- data/spec/sample_data/.keep +0 -0
- data/spec/spec_helper.rb +36 -0
- metadata +238 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4f1a1e7507e94a8e8fa04802986c0c01d9277e5b
|
4
|
+
data.tar.gz: 410d9d4a7107d40394706f01a2f2af34b482a5b1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7f58e87ad4cb1ddd5f31d4fe1d9f0672aaf1e48d65630b16a019f33e8b8d034f9d7ec727fce4f2e8c3d3d1154ada8845b421d0d9efba2ba2ddb7d3ccb9cb9348
|
7
|
+
data.tar.gz: 8331188a07b56e71855333703197016e89f634efd006e4ec5709abaf116a07afc9388ad7685f6b0bb0abda3d4b154f50f348b007f77584ad438d88ddcefa1aa5
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Nidev Plontra
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Rbitter #
|
2
|
+
[![Build Status](https://travis-ci.org/nidev/rbitter.svg?branch=master)](https://travis-ci.org/nidev/rbitter)
|
3
|
+
|
4
|
+
Rbitter is a Twitter streaming client specialized in tweet archiving with remote access via XMLRPC, which is written in Ruby.
|
5
|
+
|
6
|
+
You can save all tweets appeared on timeline and watch them later.
|
7
|
+
|
8
|
+
**Rbitter can be built into a Ruby gem by typing 'gem build rbitter.gemspec'. It is not yet uploaded due to testing.**
|
9
|
+
|
10
|
+
## Requirements ##
|
11
|
+
|
12
|
+
'bundle update' may install every gem Rbitter needs.
|
13
|
+
|
14
|
+
Ruby 2.0.0 or above
|
15
|
+
Sqlite3
|
16
|
+
Mysql(MariaDB)
|
17
|
+
|
18
|
+
## Configuration ##
|
19
|
+
You can simply manipulate default configuration file by:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ rbitter configure
|
23
|
+
```
|
24
|
+
|
25
|
+
Put your customized config.json to one of below locations.
|
26
|
+
|
27
|
+
1. $HOME/config.json
|
28
|
+
2. $HOME/.rbitter/config.json
|
29
|
+
3. ./config.json (current folder)
|
30
|
+
4. ./.rbitter/config.json (current folder)
|
31
|
+
|
32
|
+
For location #3 and #4, they're referred when #1 and #2 are not available. In those cases, *rbitter* must be executed from the directory where config.json exists.
|
33
|
+
|
34
|
+
## Set up and run ##
|
35
|
+
1. With config.json,
|
36
|
+
2. Get Twitter token and copy them properly.
|
37
|
+
2. Choose preferred ActiveRecord backend.
|
38
|
+
3. Modify default location where Twitter images are downloaded.
|
39
|
+
4. Configure XMLRPC server
|
40
|
+
5. Start by typing:
|
41
|
+
|
42
|
+
```bash
|
43
|
+
$ rbitter serve
|
44
|
+
```
|
45
|
+
## XMLRPC API ##
|
46
|
+
|
47
|
+
See XMLRPC.md
|
48
|
+
|
49
|
+
## TODO ##
|
50
|
+
* Streaming Client
|
51
|
+
* Saving JSON dump
|
52
|
+
* XMLRPC
|
53
|
+
* Receiving direct messages
|
54
|
+
|
55
|
+
## Issue report or feature request ##
|
56
|
+
It's recommended to open an issue even it seemed too small. A small flaw may result in instability or bad situation. So every feature requests/bug reports are welcomed.
|
57
|
+
|
58
|
+
Please attach stack trace, Ruby version, and detail description.
|
59
|
+
|
60
|
+
## Disclaimer ##
|
61
|
+
Rbitter is intended for personal usage. Archived data should not be shared over Internet. Please keep them secure and safe, and protect privacy.
|
62
|
+
|
63
|
+
Using sqlite3, please set permission to 0700 so that other users can not read your database file. Using mysql2, please take care of DB access privilege.
|
64
|
+
|
65
|
+
Rbitter is not responsible for integrity of data. That is, Some tweets will be dropped accidently due to Twitter API problems or your network problems. The application does its best to recover from those problems. If you find Rbitter couldn't recover even after they're resolved, please make an issue report.
|
data/Rakefile
ADDED
data/XMLRPC.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# What is RPC handle ? #
|
2
|
+
## Commands ##
|
3
|
+
### Authentication ###
|
4
|
+
### Revoke Authentication Token ###
|
5
|
+
### Echo ###
|
6
|
+
### Last Active ###
|
7
|
+
### Retriever ###
|
8
|
+
### Statistic ###
|
9
|
+
# How to write own RPC handle? #
|
10
|
+
RPC handle is a Ruby class. Writing a method in Ruby class, that's it. Names of methods are treated as XMLRPC command.
|
11
|
+
|
12
|
+
When you write a new class for your own RPC handle, you must inherit either Auth or NoAuth class from rpc/base.rb.
|
13
|
+
|
14
|
+
* class Auth < Object: Methods in a Ruby class inheriting Auth requires *auth_key* to access.
|
15
|
+
* class NoAuth < Object: Methods in a Ruby class inheriting NoAuth doesn't require *auth_key* and these XMLRPC commands can be called by anonymous user.
|
16
|
+
|
17
|
+
Filename should start with 'rh_'. It's prefix to be autoloaded by xmlrpc.rb.
|
18
|
+
|
19
|
+
Refer rpc/rh_echo.rb as an example.
|
data/bin/rbitter
ADDED
data/config.json.example
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
{
|
2
|
+
"twitter": {
|
3
|
+
"consumer_key": "",
|
4
|
+
"consumer_secret": "",
|
5
|
+
"access_token": "",
|
6
|
+
"access_token_secret": ""
|
7
|
+
},
|
8
|
+
"activerecord": "sqlite3",
|
9
|
+
"sqlite3": {
|
10
|
+
"dbfile": "rbitter.sqlite"
|
11
|
+
},
|
12
|
+
"mysql2": {
|
13
|
+
"host": "localhost",
|
14
|
+
"port": 3306,
|
15
|
+
"dbname": "archive",
|
16
|
+
"username": "",
|
17
|
+
"password": ""
|
18
|
+
},
|
19
|
+
"media_downloader": {
|
20
|
+
"cacert_path": "cacerts/cacert.pem",
|
21
|
+
"download_dir": "imgs/"
|
22
|
+
},
|
23
|
+
"xmlrpc": {
|
24
|
+
"enable": true,
|
25
|
+
"bind_host": "0.0.0.0",
|
26
|
+
"bind_port": 1400,
|
27
|
+
"auth_password": ""
|
28
|
+
},
|
29
|
+
"auth": ["username", "password"]
|
30
|
+
}
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "date"
|
5
|
+
require "twitter"
|
6
|
+
require "resolv"
|
7
|
+
|
8
|
+
require "rbitter/records"
|
9
|
+
require "rbitter/streaming"
|
10
|
+
require "rbitter/dlthread"
|
11
|
+
require "rbitter/xmlrpc"
|
12
|
+
|
13
|
+
module Rbitter
|
14
|
+
class ArcServer
|
15
|
+
def initialize
|
16
|
+
ARSupport.connect_database
|
17
|
+
|
18
|
+
if not ARSupport.prepared?
|
19
|
+
puts "Initiate database table..."
|
20
|
+
if Rbitter.env['activerecord'] == 'mysql2'
|
21
|
+
ARSupport.prepare "DEFAULT CHARSET=utf8mb4"
|
22
|
+
else
|
23
|
+
ARSupport.prepare
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
ARSupport.update_database_scheme
|
28
|
+
|
29
|
+
@t = StreamClient.new(Rbitter.env['twitter'].dup)
|
30
|
+
@dt = DLThread.new(Rbitter.env['media_downloader']['download_dir'], Rbitter.env['media_downloader']['cacert_path'])
|
31
|
+
end
|
32
|
+
|
33
|
+
def write_marker(message)
|
34
|
+
Record.create({:marker => 1,
|
35
|
+
:marker_msg => message,
|
36
|
+
:userid => nil,
|
37
|
+
:username => nil,
|
38
|
+
:tweetid => nil,
|
39
|
+
:tweet => nil,
|
40
|
+
:date => ARSupport.any_to_datestring(DateTime.now),
|
41
|
+
:rt_count => 0,
|
42
|
+
:fav_count => 0})
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_init_marker
|
46
|
+
write_marker "Archiving service started"
|
47
|
+
end
|
48
|
+
|
49
|
+
def write_halt_marker
|
50
|
+
write_marker "Archiving service halted"
|
51
|
+
end
|
52
|
+
|
53
|
+
def main_loop
|
54
|
+
begin
|
55
|
+
write_init_marker
|
56
|
+
@t.run { |a|
|
57
|
+
record = Record.find_or_initialize_by(tweetid: a['tweetid'])
|
58
|
+
record.update({:marker => 0,
|
59
|
+
:marker_msg => "normal",
|
60
|
+
:userid => a['userid'],
|
61
|
+
:username => a['screen_name'],
|
62
|
+
:tweetid => a['tweetid'],
|
63
|
+
:tweet => a['tweet'],
|
64
|
+
:date => a['date'],
|
65
|
+
:rt_count => a['rt_count'],
|
66
|
+
:fav_count => a['fav_count']})
|
67
|
+
|
68
|
+
record.save
|
69
|
+
@dt.execute_urls(a['media_urls'])
|
70
|
+
}
|
71
|
+
rescue Interrupt => e
|
72
|
+
puts ""
|
73
|
+
puts "Interrupted..."
|
74
|
+
if Rbitter.env['xmlrpc']['enable']
|
75
|
+
puts "Finishing RPCServer"
|
76
|
+
if $rpc_service_thread.alive?
|
77
|
+
$rpc_service_thread.terminate
|
78
|
+
$rpc_service_thread.join
|
79
|
+
end
|
80
|
+
end
|
81
|
+
exit 0
|
82
|
+
rescue Twitter::Error::Unauthorized => e
|
83
|
+
puts "Please configure your Twitter token on config.json."
|
84
|
+
exit -1
|
85
|
+
rescue Twitter::Error::ServerError, Resolv::ResolvError => e
|
86
|
+
puts "Service unavailable now. Retry in 5 second..."
|
87
|
+
sleep 5
|
88
|
+
retry
|
89
|
+
rescue Twitter::Error => e
|
90
|
+
puts "Twitter Error: #{e.inspect}"
|
91
|
+
exit -1
|
92
|
+
ensure
|
93
|
+
write_halt_marker
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Rbitter Archive Access console (irb)
|
4
|
+
|
5
|
+
require "xmlrpc/client"
|
6
|
+
require "ripl"
|
7
|
+
|
8
|
+
module Rbitter
|
9
|
+
class Console
|
10
|
+
def initialize
|
11
|
+
puts "Welcome to Rbitter console"
|
12
|
+
help
|
13
|
+
end
|
14
|
+
|
15
|
+
def help
|
16
|
+
puts "Predefined methods:"
|
17
|
+
puts "help - to show this message again"
|
18
|
+
puts "xmlrpc - to utilize xmlrpc"
|
19
|
+
puts "activerec - to connect and utilize Rbitter::Record"
|
20
|
+
puts "^D, 'exit' - to get out from here."
|
21
|
+
end
|
22
|
+
|
23
|
+
def activerec
|
24
|
+
ARSupport.connect_database
|
25
|
+
puts "Rbitter::Record is ready."
|
26
|
+
end
|
27
|
+
|
28
|
+
def exit
|
29
|
+
Kernel.exit(0)
|
30
|
+
end
|
31
|
+
|
32
|
+
def xmlrpc *args
|
33
|
+
if args.empty?
|
34
|
+
puts "How to use: xmlrpc (command) [params in Array]"
|
35
|
+
puts "Ex) xmlrpc rbitter.echo [\"Hello World!\"]"
|
36
|
+
puts "Ex) To call XMLRPC function with zero parameter, use nil."
|
37
|
+
return false
|
38
|
+
end
|
39
|
+
|
40
|
+
cl = XMLRPC::Client.new('localhost', '/', 1400) # TODO: External address?
|
41
|
+
if @xmlrpc_cookie.nil?
|
42
|
+
@xmlrpc_cookie = "auth_key=" + cl.call('rbitter.auth', Rbitter.env['xmlrpc']['auth'][0], Rbitter.env['xmlrpc']['auth'][1])
|
43
|
+
end
|
44
|
+
|
45
|
+
if cl.cookie != "auth_key="
|
46
|
+
if args.length <= 1 or args[1].nil?
|
47
|
+
cl.call(args[0])
|
48
|
+
else
|
49
|
+
cl.call(args[0], *args[1])
|
50
|
+
end
|
51
|
+
else
|
52
|
+
puts "Authentication failed. Check your config.json"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def start
|
57
|
+
Ripl.start :binding => binding
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Rbitter
|
2
|
+
DEFAULT_CONFIG_JSON = <<-ENDOFJSON
|
3
|
+
{
|
4
|
+
"twitter": {
|
5
|
+
"consumer_key": "",
|
6
|
+
"consumer_secret": "",
|
7
|
+
"access_token": "",
|
8
|
+
"access_token_secret": ""
|
9
|
+
},
|
10
|
+
"activerecord": "sqlite3",
|
11
|
+
"sqlite3": {
|
12
|
+
"dbfile": "rbitter.sqlite"
|
13
|
+
},
|
14
|
+
"mysql2": {
|
15
|
+
"host": "localhost",
|
16
|
+
"port": 3306,
|
17
|
+
"dbname": "archive",
|
18
|
+
"username": "",
|
19
|
+
"password": ""
|
20
|
+
},
|
21
|
+
"media_downloader": {
|
22
|
+
"cacert_path": "/cacerts/cacert.pem",
|
23
|
+
"download_dir": "imgs/"
|
24
|
+
},
|
25
|
+
"xmlrpc": {
|
26
|
+
"enable": true,
|
27
|
+
"bind_host": "0.0.0.0",
|
28
|
+
"bind_port": 1400,
|
29
|
+
"auth": {
|
30
|
+
"username": "username",
|
31
|
+
"password": "password"
|
32
|
+
},
|
33
|
+
"handles": ["/path/to/handles"]
|
34
|
+
}
|
35
|
+
}
|
36
|
+
ENDOFJSON
|
37
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "openssl"
|
5
|
+
|
6
|
+
module Rbitter
|
7
|
+
class DLThread
|
8
|
+
def initialize(dlfolder, cacert_path)
|
9
|
+
@dest = dlfolder
|
10
|
+
if not File.directory?(dlfolder)
|
11
|
+
warn "[dlthread] Given download location is not available for downloading."
|
12
|
+
warn "[dlthread] Current Dir is used instead."
|
13
|
+
@dest = "./"
|
14
|
+
end
|
15
|
+
|
16
|
+
@cacert = cacert_path
|
17
|
+
end
|
18
|
+
|
19
|
+
def execute_urls(urls)
|
20
|
+
urls.each { |url|
|
21
|
+
download_once(url)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def download_once(url)
|
27
|
+
download_task = Thread.new {
|
28
|
+
uri = URI.parse(url)
|
29
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
30
|
+
if uri.scheme.downcase == 'https'
|
31
|
+
http.use_ssl = true
|
32
|
+
http.ca_path = @cacert_path
|
33
|
+
# XXX: Fix this soon as possible.
|
34
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
35
|
+
end
|
36
|
+
|
37
|
+
http.request_get(uri.path) { |res|
|
38
|
+
case res
|
39
|
+
when Net::HTTPOK
|
40
|
+
fname = File.basename(uri.path)
|
41
|
+
if fname.nil? or fname.size < 1
|
42
|
+
fname = uri.path.gsub(/\//, "_")
|
43
|
+
end
|
44
|
+
puts "[fetch] remote: #{url} => local: #{fname}"
|
45
|
+
File.open(@dest+"/"+fname, "wb") { |file|
|
46
|
+
res.read_body { |chunk|
|
47
|
+
file.write(chunk)
|
48
|
+
}
|
49
|
+
}
|
50
|
+
end
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
download_task.run
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# XXX: Should be moved to dlthread_spec.rb
|
60
|
+
=begin
|
61
|
+
if __FILE__ == $0
|
62
|
+
t = DLThread.new(".")
|
63
|
+
t.execute_urls(["https://www.google.co.kr/images/nav_logo195.png"])
|
64
|
+
sleep 4
|
65
|
+
end
|
66
|
+
=end
|
data/lib/rbitter/env.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Rbitter
|
6
|
+
@internal_configuration = {}
|
7
|
+
|
8
|
+
class ConfigurationFileError < StandardError; end
|
9
|
+
|
10
|
+
module_function
|
11
|
+
def env
|
12
|
+
@internal_configuration
|
13
|
+
end
|
14
|
+
|
15
|
+
def env_reset
|
16
|
+
@internal_configuration.clear
|
17
|
+
end
|
18
|
+
|
19
|
+
def config_load path
|
20
|
+
open(path, 'r') { |io|
|
21
|
+
@internal_configuration = JSON.parse(io.read)
|
22
|
+
}
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def config_initialize json_path=nil
|
27
|
+
@internal_configuration = JSON.parse("{}")
|
28
|
+
|
29
|
+
unless json_path.nil?
|
30
|
+
begin
|
31
|
+
config_load(json_path)
|
32
|
+
# TODO: Configuration validation
|
33
|
+
return @internal_configuration
|
34
|
+
rescue => e
|
35
|
+
fail ConfigurationFileError, "Provided configuration can not be loaded. (#{json_path})"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Configuration default location
|
40
|
+
# Priorities
|
41
|
+
# 1. (current_dir)/config.json
|
42
|
+
# 2. (current_dir)/.rbitter/config.json
|
43
|
+
locations = Array.new
|
44
|
+
base_locations = ["config.json", ".rbitter/config.json"]
|
45
|
+
|
46
|
+
base_locations.each { |bloc|
|
47
|
+
locations.push File.join(Dir.pwd, bloc)
|
48
|
+
}
|
49
|
+
|
50
|
+
for location in locations
|
51
|
+
next unless File.file?(location)
|
52
|
+
break if config_load(location)
|
53
|
+
end
|
54
|
+
|
55
|
+
if @internal_configuration.empty?
|
56
|
+
fail ConfigurationFileError, "Can not load any configuration in [#{locations.join(', ')}]"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'http/parser'
|
4
|
+
require 'openssl'
|
5
|
+
require 'resolv'
|
6
|
+
|
7
|
+
module Twitter
|
8
|
+
module Streaming
|
9
|
+
class Connection
|
10
|
+
MODIFIED = true
|
11
|
+
attr_reader :tcp_socket_class, :ssl_socket_class
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
@tcp_socket_class = options.fetch(:tcp_socket_class) { TCPSocket }
|
15
|
+
@ssl_socket_class = options.fetch(:ssl_socket_class) { OpenSSL::SSL::SSLSocket }
|
16
|
+
end
|
17
|
+
|
18
|
+
def stream(request, response)
|
19
|
+
client_context = OpenSSL::SSL::SSLContext.new
|
20
|
+
client = @tcp_socket_class.new(Resolv.getaddress(request.uri.host), request.uri.port)
|
21
|
+
ssl_client = @ssl_socket_class.new(client, client_context)
|
22
|
+
ssl_client.connect
|
23
|
+
request.stream(ssl_client)
|
24
|
+
|
25
|
+
loop {
|
26
|
+
begin
|
27
|
+
body = ssl_client.read_nonblock(1024) # rubocop:disable AssignmentInCondition, WhileUntilModifier
|
28
|
+
response << body
|
29
|
+
rescue IO::WaitReadable
|
30
|
+
# The reason for setting 90 seconds as a timeout is documented on:
|
31
|
+
# https://dev.twitter.com/streaming/overview/connecting
|
32
|
+
r, w, e = IO.select([ssl_client], [], [], 90)
|
33
|
+
if r.nil?
|
34
|
+
# If timeout occurs
|
35
|
+
ssl_client.close
|
36
|
+
raise Twitter::Error::ServerError.new("Connection stalled")
|
37
|
+
else
|
38
|
+
# If socket is readable
|
39
|
+
retry
|
40
|
+
end
|
41
|
+
end
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "date"
|
5
|
+
|
6
|
+
module Rbitter
|
7
|
+
class Record < ActiveRecord::Base
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ARSupport
|
12
|
+
SCHEME_VERSION = 20150327
|
13
|
+
SCHEME = {
|
14
|
+
:marker => :integer, # 0 normal, 2 cut, 1 resume
|
15
|
+
:marker_msg => :string, # == 0 success, == 2 w/ message
|
16
|
+
:userid => :integer,
|
17
|
+
:username => :string,
|
18
|
+
:tweetid => :integer,
|
19
|
+
:tweet => :text, # with url unpacked
|
20
|
+
:date => :datetime,
|
21
|
+
:rt_count => :integer,
|
22
|
+
:fav_count => :integer
|
23
|
+
}
|
24
|
+
|
25
|
+
module_function
|
26
|
+
def prepared?
|
27
|
+
ActiveRecord::Base.connection.table_exists?(:records)
|
28
|
+
end
|
29
|
+
|
30
|
+
def connect_database
|
31
|
+
if Rbitter.env['activerecord'] == 'sqlite3'
|
32
|
+
warn "Warning: If you enable XMLRPC access, using sqlite is not recommended."
|
33
|
+
warn "Warning: Random crash can happen because of concurrency."
|
34
|
+
|
35
|
+
if RUBY_PLATFORM == 'java'
|
36
|
+
require "jdbc/sqlite3"
|
37
|
+
Jdbc::SQLite3.load_driver
|
38
|
+
ActiveRecord::Base.establish_connection(
|
39
|
+
adapter: 'jdbcsqlite3',
|
40
|
+
database: Rbitter.env['sqlite3']['dbfile'],
|
41
|
+
timeout: 10000) # Long timeout for slow computer
|
42
|
+
else
|
43
|
+
ActiveRecord::Base.establish_connection(
|
44
|
+
adapter: 'sqlite3',
|
45
|
+
database: Rbitter.env['sqlite3']['dbfile'],
|
46
|
+
timeout: 10000) # Long timeout for slow computer
|
47
|
+
end
|
48
|
+
elsif Rbitter.env['activerecord'] == 'mysql2'
|
49
|
+
Jdbc::MySQL.load_driver if RUBY_PLATFORM == 'java'
|
50
|
+
|
51
|
+
ActiveRecord::Base.establish_connection(
|
52
|
+
adapter: (RUBY_PLATFORM == 'java' ? 'jdbcmysql' : 'mysql2'),
|
53
|
+
host: Rbitter.env['mysql2']['host'],
|
54
|
+
port: Rbitter.env['mysql2']['port'],
|
55
|
+
database: Rbitter.env['mysql2']['dbname'],
|
56
|
+
username: Rbitter.env['mysql2']['username'],
|
57
|
+
password: Rbitter.env['mysql2']['password'],
|
58
|
+
encoding: "utf8mb4",
|
59
|
+
collation: "utf8mb4_unicode_ci")
|
60
|
+
else
|
61
|
+
raise RuntimeException.new("Unknown configuration value. 'activerecord' value should be sqlite3 or mysql2.")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def update_database_scheme
|
66
|
+
current_version = ActiveRecord::Migrator.current_version
|
67
|
+
if current_version < SCHEME_VERSION
|
68
|
+
warn "[records] Your ActiveRecord scheme is outdated."
|
69
|
+
warn "[records] Migrate... #{current_version} => #{SCHEME_VERSION}"
|
70
|
+
ActiveRecord::Migrator.migrate(File.expand_path("../records_migrate", __FILE__), SCHEME_VERSION)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def prepare option_string=""
|
75
|
+
ActiveRecord::Schema.define(version: SCHEME_VERSION) {
|
76
|
+
# MySQL specific option_string:
|
77
|
+
# utf8mb4 -> supporting UTF-8 4-byte characters (i.e. Emoji)
|
78
|
+
create_table(:records, { :options => option_string }) do |t|
|
79
|
+
SCHEME.each_key { |column|
|
80
|
+
case SCHEME[column]
|
81
|
+
when :string
|
82
|
+
t.string column
|
83
|
+
when :integer
|
84
|
+
t.integer column, :limit => 8
|
85
|
+
when :datetime
|
86
|
+
t.datetime column
|
87
|
+
when :text
|
88
|
+
t.text column
|
89
|
+
else
|
90
|
+
puts "Unexpected column type '#{SCHEME[column]}' of #{column}"
|
91
|
+
end
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
add_index :records, :tweetid
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def any_to_datestring(obj)
|
100
|
+
if obj.is_a?(String)
|
101
|
+
# try to parse it
|
102
|
+
DateTime.parse(obj).strftime("%Y-%m-%d %H:%M:%S")
|
103
|
+
elsif obj.is_a?(DateTime) or obj.is_a?(Time)
|
104
|
+
obj.strftime("%Y-%m-%d %H:%M:%S")
|
105
|
+
else
|
106
|
+
raise ArgumentError.new("Can\'t automatically extract DateTime info")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|