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