pingo 1.0.0 → 1.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8e46197fce732659a1f29c737099474746d885f9
4
- data.tar.gz: 543c8ec0bbdf0cdd48eeadb1b246d93ba427b6b4
3
+ metadata.gz: 371a37cd9c400853c5fa4e23e4fbe33ca6c1665c
4
+ data.tar.gz: 38d4970b71fc6c5bf5ae37e3b29ff219e6e0b6ee
5
5
  SHA512:
6
- metadata.gz: 2bf5b2aa38d6c5441c256831ddcfa187948823cc99a05732ee635574adba466e273688bbc07932fb32b14ecad37cc11f918bb9b08fa1d0f757a9ab8f2a3cb6b3
7
- data.tar.gz: 95e36f1837a12ca45133480a4309bc1c9ff287e8854adc6f8f819b051c3956df103572bda8c653e130b89c7932731543c23431a1adcd5bed6b2239308406ef53
6
+ metadata.gz: 723cb94874b30b16762d4d40ad8bf3f0b5c1ee8fdd7d096a95f06882e60b7a86aa8735372e9eb6ebb098c52c50cc16c51ef6f0382f86f5e740d6ca92d8e675da
7
+ data.tar.gz: 316322f0d1f131526dc3feca391ccee749c3e185a54ed5afe127fc19b5e4c322f056603a81455cd2f937879c2dff780e0298de1ef7790e635412236c9f9910de
@@ -1,3 +1,8 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.1.1
3
+ - 2.4.1
4
+ - 2.3.4
5
+ - 2.2.7
6
+ - 2.1.10
7
+ - 2.0.0
8
+ - ruby-head
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Pingo [![Gem Version](https://badge.fury.io/rb/pingo.svg)](http://badge.fury.io/rb/pingo) [![Code Climate](https://codeclimate.com/github/Kyuden/pingo/badges/gpa.svg)](https://codeclimate.com/github/Kyuden/pingo) [![wercker status](https://app.wercker.com/status/8fc989959ae4630aef746364c6ead94f/m/master "wercker status")](https://app.wercker.com/project/bykey/8fc989959ae4630aef746364c6ead94f)
1
+ # Pingo [![Gem Version](https://badge.fury.io/rb/pingo.svg)](http://badge.fury.io/rb/pingo) [![Code Climate](https://codeclimate.com/github/Kyuden/pingo/badges/gpa.svg)](https://codeclimate.com/github/Kyuden/pingo)
2
2
 
3
3
  <p><img width="400"src="http://www.fastpic.jp/images.php?file=8622347177.jpg"></p>
4
4
  Pingo provide a scatterbrain with a `pingo` command of sounding your iphone.
@@ -7,11 +7,19 @@ Pingo provide a scatterbrain with a `pingo` command of sounding your iphone.
7
7
 
8
8
  Install it yourself as:
9
9
 
10
- $ gem install pingo
10
+ ```bash
11
+ $ gem install pingo
12
+ ```
13
+
14
+ If you've done Go development before and your $GOPATH/bin directory is already in your PATH, this is an alternative installation method that fetches `pingo` into your GOPATH and builds it automatically:
15
+
16
+ ```
17
+ $ go get github.com/kyuden/pingo
18
+ ```
11
19
 
12
20
  ## Usage
13
21
 
14
- Set APPLE_ID and APPLE_PASSWORD in environment variables.
22
+ Set APPLE_ID and APPLE_PASSWORD in environment variables.
15
23
 
16
24
  ```bash
17
25
  # Set it in .zshrc, .bashrc etc...
@@ -1,20 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pingo/version"
2
4
  require "pingo/cli"
3
5
  require "json"
4
6
  require "typhoeus"
5
7
 
6
8
  module Pingo
7
- class Pingo
8
- INIT_CLIENT = 'initClient'
9
- PLAY_SOUND = 'playSound'
9
+ class Client
10
+ INIT_CLIENT = 'initClient'.freeze
11
+ PLAY_SOUND = 'playSound'.freeze
10
12
 
11
13
  class << self
12
14
  def run(model_name)
13
- new(model_name).instance_eval do
14
- partition = request_partition
15
- device_ids = request_device_ids(partition)
16
- request_sound(partition, device_ids)
17
- end
15
+ new(model_name).sound!
18
16
  end
19
17
  end
20
18
 
@@ -24,38 +22,35 @@ module Pingo
24
22
  @password = ENV['APPLE_PASSWORD']
25
23
  end
26
24
 
27
- private
28
- def request_partition
29
- post(INIT_CLIENT).headers['X-Apple-MMe-Host']
30
- end
25
+ def sound!
26
+ device_ids.each { |device_id| post(PLAY_SOUND, generate_body(device_id)) }
27
+ end
31
28
 
32
- def request_device_ids(partition)
33
- raise "partition is nil" unless partition
34
- parse_device_ids(post(INIT_CLIENT, partition))
35
- end
29
+ def device_ids
30
+ response = post(INIT_CLIENT)
31
+ raise "#{response.response_code}:#{response.status_message }" unless response.success?
32
+ parse_device_ids(response.body)
33
+ end
34
+
35
+ private
36
36
 
37
- def parse_device_ids(data)
38
- target_contents(data).map { |content| content["id"] }
37
+ def parse_device_ids(body)
38
+ target_contents(body).map { |content| content["id"] }
39
39
  end
40
40
 
41
- def target_contents(data)
42
- contents(data).find_all { |content| match_device?(content) }
41
+ def target_contents(body)
42
+ target = contents(body).find_all { |content| match_device?(content) }
43
+ target.empty? ? raise("Not found your device(iPhone#{@model_name})") : target
43
44
  end
44
45
 
45
- def contents(data)
46
- JSON.parse(data.body)['content']
46
+ def contents(body)
47
+ JSON.parse(body)['content']
47
48
  end
48
49
 
49
50
  def match_device?(params)
50
51
  params['deviceDisplayName'] =~ /#{@model_name}$/i
51
52
  end
52
53
 
53
- def request_sound(partition, device_ids)
54
- raise "partition is nil" unless partition
55
- raise "device id is nil" if Array(device_ids).empty?
56
- Array(device_ids).map { |device_id| post(PLAY_SOUND, partition, generate_body(device_id)) }
57
- end
58
-
59
54
  def generate_body(device_id)
60
55
  JSON.generate(sound_body(device_id))
61
56
  end
@@ -72,8 +67,8 @@ module Pingo
72
67
  }
73
68
  end
74
69
 
75
- def post(mode, partition="fmipmobile.icloud.com", body=nil)
76
- Typhoeus::Request.post(uri(mode, partition),
70
+ def post(type, body=nil)
71
+ Typhoeus::Request.post(uri(type),
77
72
  userpwd: "#{@username}:#{@password}",
78
73
  headers: post_headers,
79
74
  followlocation: true,
@@ -94,8 +89,8 @@ module Pingo
94
89
  }
95
90
  end
96
91
 
97
- def uri(mode, partition)
98
- "https://#{partition}/fmipservice/device/#{@username}/#{mode}"
92
+ def uri(type)
93
+ "https://fmipmobile.icloud.com/fmipservice/device/#{@username}/#{type}"
99
94
  end
100
95
  end
101
96
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require 'pingo/cli/pingo'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Pingo
2
4
  class CLI < Thor
3
5
  desc "[MODEL_NAME]", "Sound apple device"
@@ -9,7 +11,7 @@ module Pingo
9
11
  LONGDESC
10
12
 
11
13
  def pingo(model_name)
12
- Pingo.run(model_name)
14
+ Client.run(model_name)
13
15
  end
14
16
  end
15
17
  end
@@ -1,3 +1,3 @@
1
1
  module Pingo
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
data/pin.go ADDED
@@ -0,0 +1,299 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "errors"
7
+ "flag"
8
+ "fmt"
9
+ "io"
10
+ "io/ioutil"
11
+ "net/http"
12
+ "os"
13
+ "strconv"
14
+ "strings"
15
+ "time"
16
+ )
17
+
18
+ const (
19
+ AppName = "Pingo"
20
+ APIName = "FindMyiphone"
21
+ APIVertion = "2.0.2"
22
+ )
23
+
24
+ const (
25
+ ExitOK = 0 + iota
26
+
27
+ ExitError = 9 + iota
28
+ ExitParseArgsError
29
+ ExitRequestDeviceError
30
+ ExitRequestSoundError
31
+ )
32
+
33
+ var HEADER_MAP = map[string][]string{
34
+ "Content-Type": {"application/json; charset=utf-8"},
35
+ "X-Apple-Find-Api-Ver": {"2.0"},
36
+ "X-Apple-Authscheme": {"UserIdGuest"},
37
+ "X-Apple-Realm-Support": {"1.0"},
38
+ "Accept-Language": {"en-us"},
39
+ "userAgent": {"Pingo"},
40
+ "Connection": {"keep-alive"},
41
+ }
42
+
43
+ type ClientContext struct {
44
+ AppName string `json:"appName"`
45
+ AppVersion string `json:"appVersion"`
46
+ ShouldLocate bool `json:"shouldLocate"`
47
+ }
48
+
49
+ type SoundParams struct {
50
+ ClientContext `json:"clientContext"`
51
+ Device string `json:"device"`
52
+ Subject string `json:"subject"`
53
+ }
54
+
55
+ type Container struct {
56
+ Content []struct {
57
+ DeviceName string `json:"deviceDisplayName"`
58
+ DeviceId string `json:"id"`
59
+ } `json:"content"`
60
+ }
61
+
62
+ func main() {
63
+ os.Exit(NewCLI().Run(os.Args))
64
+ }
65
+
66
+ type CLI struct {
67
+ outStream, errStream io.Writer
68
+ }
69
+
70
+ func NewCLI() *CLI {
71
+ return &CLI{
72
+ outStream: os.Stdout,
73
+ errStream: os.Stderr,
74
+ }
75
+ }
76
+
77
+ func (cli *CLI) PutOutStream(format string, args ...interface{}) {
78
+ fmt.Fprintf(cli.outStream, format, args...)
79
+ }
80
+
81
+ func (cli *CLI) PutErrStream(format string, args ...interface{}) {
82
+ fmt.Fprintf(cli.errStream, format, args...)
83
+ }
84
+
85
+ func (cli *CLI) Run(args []string) int {
86
+ appleAccount, err := cli.parseArgs(args)
87
+ if err != nil {
88
+ cli.PutErrStream("Failed to parse args:\n %s\n", err)
89
+ return ExitParseArgsError
90
+ }
91
+
92
+ client := &Client{AppleAccount: appleAccount}
93
+
94
+ deviceID, err := client.RequestDeviceID()
95
+ if err != nil {
96
+ cli.PutErrStream("Failed to request device id:\n %s\n", err)
97
+ return ExitRequestDeviceError
98
+ }
99
+
100
+ if err = client.RequestSound(deviceID); err != nil {
101
+ cli.PutErrStream("Failed to request sound:\n %s\n", err)
102
+ return ExitRequestSoundError
103
+ }
104
+
105
+ cli.PrintSuccessMessage()
106
+
107
+ return ExitOK
108
+ }
109
+
110
+ type AppleAccount struct {
111
+ ID string
112
+ Pass string
113
+ ModelName string
114
+ }
115
+
116
+ func (cli *CLI) parseArgs(args []string) (*AppleAccount, error) {
117
+ appleID := os.Getenv("APPLE_ID")
118
+ applePass := os.Getenv("APPLE_PASSWORD")
119
+
120
+ flags := flag.NewFlagSet(AppName, flag.ContinueOnError)
121
+
122
+ flags.StringVar(&appleID, "apple-id", appleID, "apple id to use")
123
+ flags.StringVar(&appleID, "i", appleID, "apple id to use (short)")
124
+ flags.StringVar(&applePass, "apple-password", applePass, "apple passwaord to to")
125
+ flags.StringVar(&applePass, "p", applePass, "apple passwaord to to (short)")
126
+
127
+ if err := flags.Parse(args[1:]); err != nil {
128
+ return nil, errors.New("Faild to parse flag")
129
+ }
130
+
131
+ if appleID == "" || applePass == "" {
132
+ return nil, errors.New("APPLE ID or APPLE PASSWORD are empty")
133
+ }
134
+
135
+ modelName := flags.Arg(0)
136
+
137
+ if modelName == "" {
138
+ return nil, errors.New("Device model name is empty")
139
+ }
140
+
141
+ return &AppleAccount{ID: appleID, Pass: applePass, ModelName: modelName}, nil
142
+ }
143
+
144
+ func (c *CLI) PrintSuccessMessage() {
145
+ fmt.Println(`
146
+ 888888ba oo
147
+ 88 8b
148
+ a88aaaa8P dP 88d888b. .d8888b. .d8888b.
149
+ 88 88 88 88 88 88 88 88
150
+ 88 88 88 88 88. .88 88. .88
151
+ dP dP dP dP .8888P88 88888P
152
+ .88
153
+ d8888P
154
+ `)
155
+ return
156
+ }
157
+
158
+ type Client struct {
159
+ *AppleAccount
160
+ debug bool
161
+ }
162
+
163
+ func (c *Client) requestDeviceIDURL() string {
164
+ return fmt.Sprintf("https://fmipmobile.icloud.com/fmipservice/device/%s/initClient", c.ModelName)
165
+ }
166
+
167
+ func (c *Client) requestSoundURL() string {
168
+ return fmt.Sprintf("https://fmipmobile.icloud.com/fmipservice/device/%s/playSound", c.ModelName)
169
+ }
170
+
171
+ func (c *Client) RequestDeviceID() (string, error) {
172
+ body, err := c.getBody("POST", c.requestDeviceIDURL(), nil)
173
+ if err != nil {
174
+ return "", errors.New("getBody: " + err.Error())
175
+ }
176
+
177
+ deviceID, err := c.parseDeviceID(body)
178
+ if err != nil {
179
+ return "", errors.New("parseDeviceID: " + err.Error())
180
+ }
181
+
182
+ return deviceID, nil
183
+ }
184
+
185
+ func (c *Client) getBody(method string, url string, params io.Reader) ([]byte, error) {
186
+ resp, err := c.httpExecute(method, url, params)
187
+ if err != nil {
188
+ return nil, errors.New("httpExecute: " + err.Error())
189
+ }
190
+
191
+ bodyBytes, err := ioutil.ReadAll(resp.Body)
192
+ defer resp.Body.Close()
193
+ if err != nil {
194
+ return nil, errors.New("ReadAll: " + err.Error())
195
+ }
196
+
197
+ if c.debug {
198
+ fmt.Printf("STATUS: %s\n", resp.Status)
199
+ fmt.Println("BODY RESPONSE: " + string(bodyBytes))
200
+ }
201
+
202
+ return bodyBytes, nil
203
+ }
204
+
205
+ type HTTPExecuteError struct {
206
+ RequestHeaders string
207
+ ResponseBodyBytes []byte
208
+ Status string
209
+ StatusCode int
210
+ }
211
+
212
+ func (e HTTPExecuteError) Error() string {
213
+ return "HTTP response is not 200/OK as expected. Actual response: \n" +
214
+ "\tResponse Status: '" + e.Status + "'\n" +
215
+ "\tResponse Code: " + strconv.Itoa(e.StatusCode) + "\n" +
216
+ "\tRequest Headers: " + e.RequestHeaders + "\n" +
217
+ "\tResponse Body: " + string(e.ResponseBodyBytes)
218
+ }
219
+
220
+ func (c *Client) httpExecute(method string, url string, body io.Reader) (*http.Response, error) {
221
+ req, err := http.NewRequest(method, url, body)
222
+ if err != nil {
223
+ return nil, errors.New("NewRequest: " + err.Error())
224
+ }
225
+
226
+ req.Header = http.Header(HEADER_MAP)
227
+ req.SetBasicAuth(c.ID, c.Pass)
228
+
229
+ client := &http.Client{Timeout: time.Duration(10 * time.Second)}
230
+
231
+ if c.debug {
232
+ fmt.Printf("Request: %v\n", req)
233
+ }
234
+ resp, err := client.Do(req)
235
+ if err != nil {
236
+ return nil, errors.New("Do: " + err.Error())
237
+ }
238
+
239
+ if resp.StatusCode != 200 {
240
+ defer resp.Body.Close()
241
+ bytes, _ := ioutil.ReadAll(resp.Body)
242
+
243
+ debugHeader := ""
244
+ for k, vals := range req.Header {
245
+ for _, val := range vals {
246
+ debugHeader += "[key: " + k + ", val: " + val + "]"
247
+ }
248
+ }
249
+
250
+ return resp, HTTPExecuteError{
251
+ RequestHeaders: debugHeader,
252
+ ResponseBodyBytes: bytes,
253
+ Status: resp.Status,
254
+ StatusCode: resp.StatusCode,
255
+ }
256
+ }
257
+
258
+ return resp, nil
259
+ }
260
+
261
+ func (c *Client) parseDeviceID(body []byte) (string, error) {
262
+
263
+ var cont Container
264
+ if err := json.Unmarshal(body, &cont); err != nil {
265
+ return "", errors.New("Unmarshal: " + err.Error())
266
+ }
267
+
268
+ var deviceId string
269
+ for _, v := range cont.Content {
270
+ if strings.HasSuffix(v.DeviceName, c.ModelName) {
271
+ deviceId = v.DeviceId
272
+ break
273
+ }
274
+ }
275
+
276
+ if deviceId == "" {
277
+ return "", errors.New("Not found device id")
278
+ }
279
+
280
+ return deviceId, nil
281
+ }
282
+
283
+ func (c *Client) RequestSound(deviceId string) error {
284
+ input, err := json.Marshal(SoundParams{
285
+ ClientContext: ClientContext{AppName: APIName, AppVersion: APIVertion},
286
+ Device: deviceId,
287
+ Subject: AppName,
288
+ })
289
+
290
+ if err != nil {
291
+ return errors.New("json.Marshal: " + err.Error())
292
+ }
293
+
294
+ if _, err := c.getBody("POST", c.requestSoundURL(), bytes.NewBuffer(input)); err != nil {
295
+ return errors.New("getBody: " + err.Error())
296
+ }
297
+
298
+ return nil
299
+ }