pingo 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![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)
|
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
|
-
|
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
|
+
}
|