snapcat 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +194 -0
  7. data/Rakefile +10 -0
  8. data/lib/snapcat.rb +19 -0
  9. data/lib/snapcat/client.rb +189 -0
  10. data/lib/snapcat/crypt.rb +35 -0
  11. data/lib/snapcat/friend.rb +57 -0
  12. data/lib/snapcat/media.rb +81 -0
  13. data/lib/snapcat/requestor.rb +106 -0
  14. data/lib/snapcat/response.rb +43 -0
  15. data/lib/snapcat/snap.rb +80 -0
  16. data/lib/snapcat/snapcat_error.rb +4 -0
  17. data/lib/snapcat/timestamp.rb +17 -0
  18. data/lib/snapcat/user.rb +46 -0
  19. data/snapcat.gemspec +28 -0
  20. data/spec/snapcat/client_spec.rb +217 -0
  21. data/spec/snapcat/crypt_spec.rb +27 -0
  22. data/spec/snapcat/friend_spec.rb +22 -0
  23. data/spec/snapcat/media_spec.rb +61 -0
  24. data/spec/snapcat/response_spec.rb +71 -0
  25. data/spec/snapcat/snap_spec.rb +72 -0
  26. data/spec/snapcat/timestamp_spec.rb +32 -0
  27. data/spec/snapcat/user_spec.rb +40 -0
  28. data/spec/snapcat_spec.rb +4 -0
  29. data/spec/spec_helper.rb +13 -0
  30. data/spec/support/data_helper.rb +20 -0
  31. data/spec/support/fixture.rb +70 -0
  32. data/spec/support/minitest_spec_context.rb +7 -0
  33. data/spec/support/request_stub.rb +322 -0
  34. data/spec/support/response_helper.rb +15 -0
  35. data/spec/support/responses/block_success.json +5 -0
  36. data/spec/support/responses/delete_success.json +5 -0
  37. data/spec/support/responses/login_success.json +91 -0
  38. data/spec/support/responses/logout_success.json +1 -0
  39. data/spec/support/responses/register_failure.json +4 -0
  40. data/spec/support/responses/register_success.json +6 -0
  41. data/spec/support/responses/registeru_failure.json +4 -0
  42. data/spec/support/responses/registeru_success.json +58 -0
  43. data/spec/support/responses/send_snap_success.json +1 -0
  44. data/spec/support/responses/set_display_name_failure.json +4 -0
  45. data/spec/support/responses/set_display_name_success.json +9 -0
  46. data/spec/support/responses/unblock_success.json +10 -0
  47. data/spec/support/responses/update_email_success.json +5 -0
  48. data/spec/support/responses/update_privacy_success.json +5 -0
  49. data/spec/support/responses/updates_success.json +97 -0
  50. data/spec/support/responses/upload_success.json +1 -0
  51. data/spec/support/snaps/image_decrypted.jpg +0 -0
  52. data/spec/support/snaps/image_encrypted.jpg +0 -0
  53. data/spec/support/snaps/video_decrypted.mp4 +0 -0
  54. data/spec/support/snaps/video_encrypted.mp4 +0 -0
  55. data/spec/support/user_experience.rb +20 -0
  56. 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
@@ -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
@@ -0,0 +1,4 @@
1
+ module Snapcat
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,17 @@
1
+ module Snapcat
2
+ module Timestamp
3
+ extend self
4
+
5
+ def float
6
+ Time.now.to_f
7
+ end
8
+
9
+ def macro
10
+ float.floor
11
+ end
12
+
13
+ def micro
14
+ (float * 1000).floor
15
+ end
16
+ end
17
+ end
@@ -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