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 +4 -4
- data/.travis.yml +6 -1
- data/README.md +11 -3
- data/lib/pingo.rb +27 -32
- data/lib/pingo/cli.rb +2 -0
- data/lib/pingo/cli/pingo.rb +3 -1
- data/lib/pingo/version.rb +1 -1
- data/pin.go +299 -0
- data/spec/fixtures/cassette_library/invalid_interaction.yml +65 -0
- data/spec/fixtures/cassette_library/valid_interaction.yml +137 -0
- data/spec/pingo_spec.rb +20 -84
- metadata +8 -16
- data/spec/fixtures/cassette_library/init_client/device_id/invalid_interaction.yml +0 -253
- data/spec/fixtures/cassette_library/init_client/device_id/valid_interaction.yml +0 -253
- data/spec/fixtures/cassette_library/init_client/partition/invalid_interaction.yml +0 -52
- data/spec/fixtures/cassette_library/init_client/partition/valid_interaction.yml +0 -56
- data/spec/fixtures/cassette_library/play_sound/invalid_interaction.yml +0 -56
- data/spec/fixtures/cassette_library/play_sound/valid_interaction.yml +0 -91
- data/wercker.yml +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 371a37cd9c400853c5fa4e23e4fbe33ca6c1665c
|
4
|
+
data.tar.gz: 38d4970b71fc6c5bf5ae37e3b29ff219e6e0b6ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 723cb94874b30b16762d4d40ad8bf3f0b5c1ee8fdd7d096a95f06882e60b7a86aa8735372e9eb6ebb098c52c50cc16c51ef6f0382f86f5e740d6ca92d8e675da
|
7
|
+
data.tar.gz: 316322f0d1f131526dc3feca391ccee749c3e185a54ed5afe127fc19b5e4c322f056603a81455cd2f937879c2dff780e0298de1ef7790e635412236c9f9910de
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Pingo [](http://badge.fury.io/rb/pingo) [](https://codeclimate.com/github/Kyuden/pingo)
|
1
|
+
# Pingo [](http://badge.fury.io/rb/pingo) [](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
|
-
|
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...
|
data/lib/pingo.rb
CHANGED
@@ -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
|
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).
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
25
|
+
def sound!
|
26
|
+
device_ids.each { |device_id| post(PLAY_SOUND, generate_body(device_id)) }
|
27
|
+
end
|
31
28
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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(
|
38
|
-
target_contents(
|
37
|
+
def parse_device_ids(body)
|
38
|
+
target_contents(body).map { |content| content["id"] }
|
39
39
|
end
|
40
40
|
|
41
|
-
def target_contents(
|
42
|
-
contents(
|
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(
|
46
|
-
JSON.parse(
|
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(
|
76
|
-
Typhoeus::Request.post(uri(
|
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(
|
98
|
-
"https
|
92
|
+
def uri(type)
|
93
|
+
"https://fmipmobile.icloud.com/fmipservice/device/#{@username}/#{type}"
|
99
94
|
end
|
100
95
|
end
|
101
96
|
end
|
data/lib/pingo/cli.rb
CHANGED
data/lib/pingo/cli/pingo.rb
CHANGED
@@ -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
|
-
|
14
|
+
Client.run(model_name)
|
13
15
|
end
|
14
16
|
end
|
15
17
|
end
|
data/lib/pingo/version.rb
CHANGED
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
|
+
}
|