snapcat 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +194 -0
- data/Rakefile +10 -0
- data/lib/snapcat.rb +19 -0
- data/lib/snapcat/client.rb +189 -0
- data/lib/snapcat/crypt.rb +35 -0
- data/lib/snapcat/friend.rb +57 -0
- data/lib/snapcat/media.rb +81 -0
- data/lib/snapcat/requestor.rb +106 -0
- data/lib/snapcat/response.rb +43 -0
- data/lib/snapcat/snap.rb +80 -0
- data/lib/snapcat/snapcat_error.rb +4 -0
- data/lib/snapcat/timestamp.rb +17 -0
- data/lib/snapcat/user.rb +46 -0
- data/snapcat.gemspec +28 -0
- data/spec/snapcat/client_spec.rb +217 -0
- data/spec/snapcat/crypt_spec.rb +27 -0
- data/spec/snapcat/friend_spec.rb +22 -0
- data/spec/snapcat/media_spec.rb +61 -0
- data/spec/snapcat/response_spec.rb +71 -0
- data/spec/snapcat/snap_spec.rb +72 -0
- data/spec/snapcat/timestamp_spec.rb +32 -0
- data/spec/snapcat/user_spec.rb +40 -0
- data/spec/snapcat_spec.rb +4 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/support/data_helper.rb +20 -0
- data/spec/support/fixture.rb +70 -0
- data/spec/support/minitest_spec_context.rb +7 -0
- data/spec/support/request_stub.rb +322 -0
- data/spec/support/response_helper.rb +15 -0
- data/spec/support/responses/block_success.json +5 -0
- data/spec/support/responses/delete_success.json +5 -0
- data/spec/support/responses/login_success.json +91 -0
- data/spec/support/responses/logout_success.json +1 -0
- data/spec/support/responses/register_failure.json +4 -0
- data/spec/support/responses/register_success.json +6 -0
- data/spec/support/responses/registeru_failure.json +4 -0
- data/spec/support/responses/registeru_success.json +58 -0
- data/spec/support/responses/send_snap_success.json +1 -0
- data/spec/support/responses/set_display_name_failure.json +4 -0
- data/spec/support/responses/set_display_name_success.json +9 -0
- data/spec/support/responses/unblock_success.json +10 -0
- data/spec/support/responses/update_email_success.json +5 -0
- data/spec/support/responses/update_privacy_success.json +5 -0
- data/spec/support/responses/updates_success.json +97 -0
- data/spec/support/responses/upload_success.json +1 -0
- data/spec/support/snaps/image_decrypted.jpg +0 -0
- data/spec/support/snaps/image_encrypted.jpg +0 -0
- data/spec/support/snaps/video_decrypted.mp4 +0 -0
- data/spec/support/snaps/video_encrypted.mp4 +0 -0
- data/spec/support/user_experience.rb +20 -0
- metadata +218 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
module Snapcat
|
2
|
+
module Crypt
|
3
|
+
extend self
|
4
|
+
|
5
|
+
CIPHER = 'AES-128-ECB'
|
6
|
+
ENCRYPTION_KEY = 'M02cnQ51Ji97vwT4'
|
7
|
+
|
8
|
+
def decrypt(data)
|
9
|
+
cipher = OpenSSL::Cipher.new(CIPHER)
|
10
|
+
cipher.decrypt
|
11
|
+
cipher.key = ENCRYPTION_KEY
|
12
|
+
decrypted_data = ''
|
13
|
+
|
14
|
+
data.bytes.each_slice(16) do |slice|
|
15
|
+
decrypted_data += cipher.update(slice.map(&:chr).join)
|
16
|
+
end
|
17
|
+
|
18
|
+
decrypted_data += cipher.final
|
19
|
+
end
|
20
|
+
|
21
|
+
def encrypt(data)
|
22
|
+
cipher = OpenSSL::Cipher.new(CIPHER)
|
23
|
+
cipher.encrypt
|
24
|
+
cipher.key = ENCRYPTION_KEY
|
25
|
+
cipher.update(pkcs5_pad(data)) + cipher.final
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def pkcs5_pad(data, blocksize = 16)
|
31
|
+
pad = blocksize - (data.length % blocksize)
|
32
|
+
"#{data}#{pad.chr * pad}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Snapcat
|
2
|
+
class Friend
|
3
|
+
ALLOWED_FIELD_CONVERSIONS = {
|
4
|
+
can_see_custom_stories: :can_see_custom_stories,
|
5
|
+
display: :display_name,
|
6
|
+
name: :username,
|
7
|
+
type: :type
|
8
|
+
}
|
9
|
+
|
10
|
+
attr_reader *ALLOWED_FIELD_CONVERSIONS.values
|
11
|
+
|
12
|
+
def initialize(data = {})
|
13
|
+
humanize_data(data)
|
14
|
+
@type = Type.new(@type)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def humanize_data(data)
|
20
|
+
ALLOWED_FIELD_CONVERSIONS.each do |api_field, human_field|
|
21
|
+
instance_variable_set(
|
22
|
+
"@#{human_field}",
|
23
|
+
data[human_field] || data[api_field]
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Type
|
29
|
+
CONFIRMED = 0
|
30
|
+
UNCONFIRMED = 1
|
31
|
+
BLOCKED = 2
|
32
|
+
DELETED = 3
|
33
|
+
|
34
|
+
attr_reader :code
|
35
|
+
|
36
|
+
def initialize(code)
|
37
|
+
@code = code
|
38
|
+
end
|
39
|
+
|
40
|
+
def blocked?
|
41
|
+
@code == BLOCKED
|
42
|
+
end
|
43
|
+
|
44
|
+
def confirmed?
|
45
|
+
@code == CONFIRMED
|
46
|
+
end
|
47
|
+
|
48
|
+
def deleted?
|
49
|
+
@code == DELETED
|
50
|
+
end
|
51
|
+
|
52
|
+
def unconfirmed?
|
53
|
+
@code == UNCONFIRMED
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Snapcat
|
2
|
+
class Media
|
3
|
+
def initialize(data, type_code = nil)
|
4
|
+
@data = Crypt.decrypt(data)
|
5
|
+
@type = Type.new(code: type_code, data: @data)
|
6
|
+
end
|
7
|
+
|
8
|
+
def image?
|
9
|
+
@type.image?
|
10
|
+
end
|
11
|
+
|
12
|
+
def file_extension
|
13
|
+
if image?
|
14
|
+
'jpg'
|
15
|
+
elsif video?
|
16
|
+
'mp4'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate_id(username)
|
21
|
+
"#{username.upcase}~#{Timestamp.macro}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
@data
|
26
|
+
end
|
27
|
+
|
28
|
+
def type_code
|
29
|
+
@type.code
|
30
|
+
end
|
31
|
+
|
32
|
+
def video?
|
33
|
+
@type.video?
|
34
|
+
end
|
35
|
+
|
36
|
+
class Type
|
37
|
+
IMAGE = 0
|
38
|
+
VIDEO = 1
|
39
|
+
VIDEO_NOAUDIO = 2
|
40
|
+
FRIEND_REQUEST = 3
|
41
|
+
FRIEND_REQUEST_IMAGE = 4
|
42
|
+
FRIEND_REQUEST_VIDEO = 5
|
43
|
+
FRIEND_REQUEST_VIDEO_NOAUDIO = 6
|
44
|
+
|
45
|
+
attr_reader :code
|
46
|
+
|
47
|
+
def initialize(options = {})
|
48
|
+
@code = code_from(options[:code], options[:data])
|
49
|
+
end
|
50
|
+
|
51
|
+
def image?
|
52
|
+
[IMAGE, FRIEND_REQUEST_IMAGE].include? @code
|
53
|
+
end
|
54
|
+
|
55
|
+
def video?
|
56
|
+
[
|
57
|
+
VIDEO, VIDEO_NOAUDIO, FRIEND_REQUEST_VIDEO, FRIEND_REQUEST_VIDEO_NOAUDIO
|
58
|
+
].include? @code
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def code_from(code, data)
|
64
|
+
if code
|
65
|
+
code
|
66
|
+
else
|
67
|
+
code_from_data(data)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def code_from_data(data)
|
72
|
+
case data.to_s[0..1]
|
73
|
+
when "\x00\x00".force_encoding('ASCII-8BIT')
|
74
|
+
VIDEO
|
75
|
+
when "\xFF\xD8".force_encoding('ASCII-8BIT')
|
76
|
+
IMAGE
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Snapcat
|
2
|
+
class Requestor
|
3
|
+
include HTTMultiParty
|
4
|
+
|
5
|
+
APP_VERSION = '6.0.0'
|
6
|
+
SECRET = 'iEk21fuwZApXlz93750dmW22pw389dPwOk'
|
7
|
+
STATIC_TOKEN = 'm198sOkJEn37DjqZ32lpRu76xmw288xSQ9'
|
8
|
+
HASH_PATTERN = '0001110111101110001111010101111011010001001110011000110001000110'
|
9
|
+
|
10
|
+
base_uri 'https://feelinsonice-hrd.appspot.com/bq/'
|
11
|
+
|
12
|
+
def initialize(username)
|
13
|
+
@auth_token = STATIC_TOKEN
|
14
|
+
@username = username
|
15
|
+
end
|
16
|
+
|
17
|
+
def request(endpoint, data = {})
|
18
|
+
response = self.class.post(
|
19
|
+
"/#{endpoint}",
|
20
|
+
{ body: merge_defaults_with(data) }
|
21
|
+
)
|
22
|
+
|
23
|
+
additional_fields = additional_fields_for(data)
|
24
|
+
result = Snapcat::Response.new(response, additional_fields)
|
25
|
+
|
26
|
+
auth_token_from(result, endpoint)
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
def request_media(snap_id)
|
31
|
+
response = self.class.post(
|
32
|
+
'/blob',
|
33
|
+
{ body: merge_defaults_with({ id: snap_id, username: @username }) }
|
34
|
+
)
|
35
|
+
|
36
|
+
Response.new(response)
|
37
|
+
end
|
38
|
+
|
39
|
+
def request_with_username(endpoint, data = {})
|
40
|
+
request(endpoint, data.merge({ username: @username }))
|
41
|
+
end
|
42
|
+
|
43
|
+
def request_upload(data, type = nil)
|
44
|
+
encrypted_data = Crypt.encrypt(data)
|
45
|
+
media = Media.new(encrypted_data, type)
|
46
|
+
file_extension = media.file_extension
|
47
|
+
|
48
|
+
begin
|
49
|
+
file = Tempfile.new(['snap', ".#{file_extension}"])
|
50
|
+
file.write(encrypted_data)
|
51
|
+
file.rewind
|
52
|
+
|
53
|
+
return request_with_username(
|
54
|
+
'upload',
|
55
|
+
data: file,
|
56
|
+
media_id: media.generate_id(@username),
|
57
|
+
type: media.type_code
|
58
|
+
)
|
59
|
+
ensure
|
60
|
+
file.close
|
61
|
+
file.unlink
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def additional_fields_for(data)
|
68
|
+
if data[:media_id]
|
69
|
+
{ media_id: data[:media_id] }
|
70
|
+
else
|
71
|
+
{}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def auth_token_from(result, endpoint)
|
76
|
+
if endpoint == 'logout'
|
77
|
+
@auth_token = STATIC_TOKEN
|
78
|
+
else
|
79
|
+
@auth_token = result.auth_token || @auth_token
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def built_token(auth_token, timestamp)
|
84
|
+
hash_a = Digest::SHA256.new << "#{SECRET}#{auth_token}"
|
85
|
+
hash_b = Digest::SHA256.new << "#{timestamp}#{SECRET}"
|
86
|
+
|
87
|
+
HASH_PATTERN.split(//).each_index.inject('') do |final_string, index|
|
88
|
+
if HASH_PATTERN[index] == '1'
|
89
|
+
final_string << hash_b.to_s[index].to_s
|
90
|
+
else
|
91
|
+
final_string << hash_a.to_s[index].to_s
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def merge_defaults_with(data)
|
97
|
+
now = Timestamp.micro
|
98
|
+
|
99
|
+
data.merge!({
|
100
|
+
req_token: built_token(@auth_token, now),
|
101
|
+
timestamp: now,
|
102
|
+
version: APP_VERSION
|
103
|
+
})
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Snapcat
|
2
|
+
class Response
|
3
|
+
attr_reader :code, :data, :http_success
|
4
|
+
|
5
|
+
def initialize(response, additional_fields = {})
|
6
|
+
@data = formatted_result(response).merge(additional_fields)
|
7
|
+
@code = response.code
|
8
|
+
@http_success = response.success?
|
9
|
+
end
|
10
|
+
|
11
|
+
def auth_token
|
12
|
+
@data[:auth_token]
|
13
|
+
end
|
14
|
+
|
15
|
+
def success?
|
16
|
+
if !@data[:logged].nil?
|
17
|
+
!!@data[:logged]
|
18
|
+
else
|
19
|
+
@http_success
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def response_empty?(response)
|
26
|
+
response.body.to_s.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def formatted_result(response)
|
30
|
+
if !response_empty?(response)
|
31
|
+
if response.content_type == 'application/octet-stream'
|
32
|
+
{ media: Media.new(response.body) }
|
33
|
+
elsif response.content_type == 'application/json'
|
34
|
+
JSON.parse(response.body, symbolize_names: true)
|
35
|
+
else
|
36
|
+
{}
|
37
|
+
end
|
38
|
+
else
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/snapcat/snap.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Snapcat
|
2
|
+
class Snap
|
3
|
+
ALLOWED_FIELD_CONVERSIONS = {
|
4
|
+
broadcast: :broadcast,
|
5
|
+
broadcast_action_text: :broadcast_action_text,
|
6
|
+
broadcast_hide_timer: :broadcast_hide_timer,
|
7
|
+
broadcast_url: :broadcast_url,
|
8
|
+
c: :screenshot_count,
|
9
|
+
c_id: :media_id,
|
10
|
+
id: :id,
|
11
|
+
m: :media_type,
|
12
|
+
rp: :recipient,
|
13
|
+
sn: :sender,
|
14
|
+
st: :status,
|
15
|
+
sts: :sent,
|
16
|
+
ts: :opened
|
17
|
+
}
|
18
|
+
|
19
|
+
attr_reader *ALLOWED_FIELD_CONVERSIONS.values
|
20
|
+
|
21
|
+
def initialize(data = {})
|
22
|
+
humanize_data(data)
|
23
|
+
@status = Status.new(@status)
|
24
|
+
@media_type = Media::Type.new(code: @media_type)
|
25
|
+
end
|
26
|
+
|
27
|
+
def received?
|
28
|
+
!sent?
|
29
|
+
end
|
30
|
+
|
31
|
+
def sent?
|
32
|
+
!!media_id
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def humanize_data(data)
|
38
|
+
ALLOWED_FIELD_CONVERSIONS.each do |api_field, human_field|
|
39
|
+
instance_variable_set(
|
40
|
+
"@#{human_field}",
|
41
|
+
data[human_field] || data[api_field]
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Status
|
47
|
+
NONE = -1
|
48
|
+
SENT = 0
|
49
|
+
DELIVERED = 1
|
50
|
+
OPENED = 2
|
51
|
+
SCREENSHOT = 3
|
52
|
+
|
53
|
+
attr_reader :code
|
54
|
+
|
55
|
+
def initialize(code)
|
56
|
+
@code = code
|
57
|
+
end
|
58
|
+
|
59
|
+
def delivered?
|
60
|
+
@code == DELIVERED
|
61
|
+
end
|
62
|
+
|
63
|
+
def opened?
|
64
|
+
@code == OPENED
|
65
|
+
end
|
66
|
+
|
67
|
+
def sent?
|
68
|
+
@code == SENT
|
69
|
+
end
|
70
|
+
|
71
|
+
def screenshot?
|
72
|
+
@code == SCREENSHOT
|
73
|
+
end
|
74
|
+
|
75
|
+
def none?
|
76
|
+
@code == NONE
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/snapcat/user.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Snapcat
|
2
|
+
class User
|
3
|
+
module Privacy
|
4
|
+
EVERYONE = 0
|
5
|
+
FRIENDS = 1
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :data, :friends, :snaps_sent, :snaps_received
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@friends = []
|
12
|
+
@snaps_sent = []
|
13
|
+
@snaps_received = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def data=(data)
|
17
|
+
set_friends(data[:friends])
|
18
|
+
set_snaps(data[:snaps])
|
19
|
+
@data = data
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def set_friends(friends)
|
25
|
+
@friends = []
|
26
|
+
|
27
|
+
friends.each do |friend_data|
|
28
|
+
@friends << Friend.new(friend_data)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_snaps(snaps)
|
33
|
+
@snaps_received = []
|
34
|
+
@snaps_sent = []
|
35
|
+
|
36
|
+
snaps.each do |snap_data|
|
37
|
+
snap = Snap.new(snap_data)
|
38
|
+
if snap.sent?
|
39
|
+
@snaps_sent << snap
|
40
|
+
else
|
41
|
+
@snaps_received << snap
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|