snapcat 0.0.1
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 +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
|