xiaomi-push 0.2.4 → 0.3.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/.circleci/config.yml +13 -0
- data/CHANGELOG.md +31 -0
- data/Gemfile +0 -1
- data/README.md +180 -44
- data/Rakefile +57 -26
- data/bin/xmp +1 -1
- data/lib/xiaomi/push.rb +6 -3
- data/lib/xiaomi/push/client.rb +70 -16
- data/lib/xiaomi/push/commands.rb +1 -0
- data/lib/xiaomi/push/commands/feedback.rb +4 -5
- data/lib/xiaomi/push/commands/message.rb +21 -12
- data/lib/xiaomi/push/commands/user.rb +69 -0
- data/lib/xiaomi/push/const.rb +20 -2
- data/lib/xiaomi/push/platforms/android.rb +17 -0
- data/lib/xiaomi/push/platforms/ios.rb +20 -0
- data/lib/xiaomi/push/services/feedback.rb +9 -2
- data/lib/xiaomi/push/services/job.rb +36 -0
- data/lib/xiaomi/push/services/message.rb +49 -56
- data/lib/xiaomi/push/services/messages.rb +91 -0
- data/lib/xiaomi/push/services/messages/android.rb +11 -1
- data/lib/xiaomi/push/services/messages/base.rb +108 -6
- data/lib/xiaomi/push/services/messages/ios.rb +21 -1
- data/lib/xiaomi/push/services/topic.rb +39 -2
- data/lib/xiaomi/push/services/user.rb +60 -0
- data/lib/xiaomi/push/version.rb +1 -1
- data/xiaomi-push.gemspec +8 -8
- metadata +27 -51
- data/.travis.yml +0 -3
- data/lib/xiaomi/push/devices/android.rb +0 -6
- data/lib/xiaomi/push/devices/ios.rb +0 -9
- data/lib/xiaomi/push/services/multi_messages.rb +0 -0
@@ -5,8 +5,15 @@ require 'xiaomi/push/services/messages/android'
|
|
5
5
|
module Xiaomi
|
6
6
|
module Push
|
7
7
|
module Services
|
8
|
+
# 单条消息类 API
|
9
|
+
#
|
10
|
+
# 允许向单个设备或多个设备发送同样的推送消息
|
11
|
+
#
|
12
|
+
# 设备的标识支持 reg_id/alias/user/topic/topics/all
|
13
|
+
#
|
14
|
+
# @attr [Client] context
|
8
15
|
class Message
|
9
|
-
|
16
|
+
# 消息类型模板
|
10
17
|
MESSAGE_TYPE = {
|
11
18
|
reg_id: {
|
12
19
|
uri: 'regid',
|
@@ -16,6 +23,10 @@ module Xiaomi
|
|
16
23
|
uri: 'alias',
|
17
24
|
query: 'alias'
|
18
25
|
},
|
26
|
+
user: {
|
27
|
+
uri: 'user_account',
|
28
|
+
query: 'user_account'
|
29
|
+
},
|
19
30
|
topic: {
|
20
31
|
uri: 'topic',
|
21
32
|
query: 'topic'
|
@@ -35,35 +46,67 @@ module Xiaomi
|
|
35
46
|
}
|
36
47
|
|
37
48
|
attr_reader :context
|
49
|
+
|
38
50
|
def initialize(context)
|
39
51
|
@context = context
|
40
52
|
end
|
41
53
|
|
54
|
+
# 推送消息
|
55
|
+
#
|
56
|
+
# @see https://dev.mi.com/console/doc/detail?pId=1163#_0
|
57
|
+
#
|
58
|
+
# @param (see Xiaomi::Push::Message::Base#initialize)
|
59
|
+
# @param [Hash, Message::IOS, Message::Android] options Hash 结构消息体 (详见 {Xiaomi::Push::Message::IOS}, {Message::Android})
|
60
|
+
# @return [Hash] 小米返回数据结构
|
61
|
+
#
|
62
|
+
# @raise [RequestError] 推送消息不满足 reg_id/alias/user/topic/topics/all 会引发异常
|
42
63
|
def send(**options)
|
43
64
|
type, value = fetch_message_type(options)
|
44
65
|
if type && value
|
45
66
|
url = @context.build_uri("message/#{type[:uri]}")
|
46
67
|
if options[:message].kind_of?Xiaomi::Push::Message::Base
|
47
68
|
options[:message].type(type[:query], value)
|
48
|
-
params = options[:message].
|
69
|
+
params = options[:message].to_params
|
49
70
|
else
|
50
71
|
params = options[:message]
|
51
72
|
params[type[:query].to_sym] = value
|
52
73
|
end
|
53
74
|
|
54
|
-
|
55
|
-
data = MultiJson.load r
|
75
|
+
@context.post(url, params)
|
56
76
|
else
|
57
|
-
raise Xiaomi::Push::RequestError, '
|
77
|
+
raise Xiaomi::Push::RequestError, '无效的消息类型,请检查是否符合这些类型: reg_id/alias/topic/topics/all'
|
58
78
|
end
|
59
79
|
end
|
60
80
|
|
81
|
+
# 获取消息的统计数据
|
82
|
+
#
|
83
|
+
# @example 获取 2017-09-01 到 2017-09-30 应用 com.icyleaf.app.helloworld 统计数据
|
84
|
+
# counters('20170901', '20170930', 'com.icyleaf.app.helloworld')
|
85
|
+
#
|
86
|
+
# @see https://dev.mi.com/console/doc/detail?pId=1163#_2
|
87
|
+
#
|
88
|
+
# @param [String] start_date 开始日期,格式 yyyyMMdd
|
89
|
+
# @param [String] end_date 结束日期,必须小于 30 天。格式 yyyyMMdd
|
90
|
+
# @param [String] package_name 包名,Android 为 package name,iOS 为 Bundle identifier
|
91
|
+
# @return [Hash] 小米返回数据结构
|
92
|
+
def counters(start_date, end_date, package_name)
|
93
|
+
url = @context.build_uri('stats/message/counters')
|
94
|
+
params = {
|
95
|
+
start_date: start_date,
|
96
|
+
end_date: end_date,
|
97
|
+
restricted_package_name: package_name
|
98
|
+
}
|
99
|
+
|
100
|
+
@context.get(url, params)
|
101
|
+
end
|
102
|
+
|
61
103
|
private
|
62
104
|
|
105
|
+
# 获取消息类型
|
63
106
|
def fetch_message_type(data)
|
64
107
|
type, value = nil
|
65
108
|
MESSAGE_TYPE.select do |k,v|
|
66
|
-
if data.has_key?k
|
109
|
+
if data.has_key?(k)
|
67
110
|
type = v
|
68
111
|
value = data[k]
|
69
112
|
break
|
@@ -72,56 +115,6 @@ module Xiaomi
|
|
72
115
|
|
73
116
|
[type, value]
|
74
117
|
end
|
75
|
-
|
76
|
-
def valid?(params)
|
77
|
-
validates = {
|
78
|
-
'payload' => {
|
79
|
-
require: true,
|
80
|
-
},
|
81
|
-
'restricted_package_name' => {
|
82
|
-
require: true,
|
83
|
-
},
|
84
|
-
'pass_through' => {
|
85
|
-
require: true,
|
86
|
-
},
|
87
|
-
'title' => {
|
88
|
-
require: true,
|
89
|
-
},
|
90
|
-
'description' => {
|
91
|
-
require: true,
|
92
|
-
},
|
93
|
-
'notify_type' => {
|
94
|
-
require: true,
|
95
|
-
values: {
|
96
|
-
'DEFAULT_ALL' => -1,
|
97
|
-
'DEFAULT_SOUND' => 1,
|
98
|
-
'DEFAULT_VIBRATE' => 2,
|
99
|
-
'DEFAULT_LIGHTS' => 3
|
100
|
-
}
|
101
|
-
},
|
102
|
-
'time_to_live' => {
|
103
|
-
require: false,
|
104
|
-
},
|
105
|
-
'time_to_send' => {
|
106
|
-
require: false,
|
107
|
-
},
|
108
|
-
'notify_id' => {
|
109
|
-
require: false,
|
110
|
-
},
|
111
|
-
'extra.sound_uri' => {
|
112
|
-
require: false,
|
113
|
-
},
|
114
|
-
'extra.ticker' => {
|
115
|
-
require: false,
|
116
|
-
},
|
117
|
-
'extra.notify_foreground' => {
|
118
|
-
require: false,
|
119
|
-
},
|
120
|
-
'extra.notify_effect' => {
|
121
|
-
require: false,
|
122
|
-
},
|
123
|
-
}
|
124
|
-
end
|
125
118
|
end
|
126
119
|
end
|
127
120
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'xiaomi/push/services/messages/base'
|
2
|
+
require 'xiaomi/push/services/messages/ios'
|
3
|
+
require 'xiaomi/push/services/messages/android'
|
4
|
+
|
5
|
+
module Xiaomi
|
6
|
+
module Push
|
7
|
+
module Services
|
8
|
+
# 消息类 API
|
9
|
+
#
|
10
|
+
# 允许向多个设备发送不同的推送消息
|
11
|
+
#
|
12
|
+
# 设备的标识支持 reg_id/alias/user_account
|
13
|
+
#
|
14
|
+
# @attr [Client] context
|
15
|
+
class Messages
|
16
|
+
# 消息类型模板
|
17
|
+
MESSAGE_TYPE = {
|
18
|
+
reg_id: {
|
19
|
+
uri: 'regids',
|
20
|
+
keys: [:reg_id, :regid, :registration_id]
|
21
|
+
},
|
22
|
+
alias: {
|
23
|
+
uri: 'aliases',
|
24
|
+
keys: [:alias, :aliass, :aliases]
|
25
|
+
},
|
26
|
+
user: {
|
27
|
+
uri: 'user_accounts',
|
28
|
+
keys: [:user, :account, :useraccount, :user_account]
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
attr_reader :context
|
33
|
+
|
34
|
+
def initialize(context)
|
35
|
+
@context = context
|
36
|
+
end
|
37
|
+
|
38
|
+
# 推送消息
|
39
|
+
#
|
40
|
+
# @see https://dev.mi.com/console/doc/detail?pId=1163#_1_0
|
41
|
+
#
|
42
|
+
# @param [Hash] type 发送消息类型,可选 :reg_id, :alias, :user
|
43
|
+
# @param [Array] messages 消息结构消息体的数组 (详见 {Xiaomi::Push::Message::IOS}, {Message::Android})
|
44
|
+
# @return [Hash] 小米返回数据结构
|
45
|
+
#
|
46
|
+
# @raise [RequestError] 推送消息不满足 reg_id/alias/user 会引发异常
|
47
|
+
# @raise [RequestError] 消息体没有包含关键 target key 会引发异常
|
48
|
+
# @raise [RequestError] messages 不是数组存储的消息体时会引发异常
|
49
|
+
def send(type, messages = [])
|
50
|
+
url = @context.build_uri("multi_messages/#{request_uri(type)}")
|
51
|
+
params = request_params(type, messages)
|
52
|
+
@context.post(url, params)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# 获取消息类型
|
58
|
+
def request_uri(type)
|
59
|
+
MESSAGE_TYPE[type][:uri]
|
60
|
+
rescue NoMethodError
|
61
|
+
raise RequestError, '无效的消息类型,请检查是否符合这些类型: reg_id/alias/user'
|
62
|
+
end
|
63
|
+
|
64
|
+
def request_params(type, messages)
|
65
|
+
raise RequestError, '消息必须是数组类型' unless messages.kind_of?(Array)
|
66
|
+
|
67
|
+
messages.each_with_object([]) do |message, obj|
|
68
|
+
message = options[:message].to_params if message.kind_of?(Xiaomi::Push::Message::Base)
|
69
|
+
target_key = target_key(type, message)
|
70
|
+
|
71
|
+
raise RequestError, "#{type.to_s} 消息缺失关键 Key,可设置为 #{MESSAGE_TYPE[type][:keys].join('/')} 均可:#{message}" unless target_key
|
72
|
+
|
73
|
+
obj.push({
|
74
|
+
target: message.delete(target_key),
|
75
|
+
message: message,
|
76
|
+
})
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def target_key(type, message)
|
81
|
+
keys = MESSAGE_TYPE[type][:keys]
|
82
|
+
message.each do |key, _|
|
83
|
+
return key if keys.include?(key)
|
84
|
+
end
|
85
|
+
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -1,8 +1,18 @@
|
|
1
1
|
module Xiaomi
|
2
2
|
module Push
|
3
3
|
module Message
|
4
|
+
# Android 消息数据体
|
5
|
+
#
|
6
|
+
# @attr [String] title 标题
|
7
|
+
# @attr [String] description 描述
|
8
|
+
# @attr [String] badge 角标, 默认 1
|
9
|
+
# @attr [String] sound 声音,默认 default
|
10
|
+
# @attr [String] pass_through 是否为穿透, 取值 0(默认,通知栏消息)/1(穿透)
|
11
|
+
# @attr [String] notify_type 提醒类型,取值 DEFAULT_ALL(默认)/DEFAULT_SOUND(提示音)/DEFAULT_VIBRATE(振动)/DEFAULT_LIGHTS(指示灯)
|
12
|
+
# @attr [Integer] notify_id 提醒,默认情况下,通知栏只显示一条推送消息。如果通知栏要显示多条推送消息,需要针对不同的消息设置不同的 notify_id
|
4
13
|
class Android < Base
|
5
|
-
attr_accessor :title, :description, :badge, :sound, :pass_through, :notify_type, :notify_id
|
14
|
+
attr_accessor :title, :description, :badge, :sound, :pass_through, :notify_type, :notify_id
|
15
|
+
|
6
16
|
def initialize(**params)
|
7
17
|
@title = params[:title]
|
8
18
|
@description = params[:description]
|
@@ -1,9 +1,23 @@
|
|
1
1
|
module Xiaomi
|
2
2
|
module Push
|
3
3
|
module Message
|
4
|
+
# 消息体的基本数据
|
5
|
+
#
|
6
|
+
# @abstract
|
7
|
+
# @attr [String] registration_id reg id
|
8
|
+
# @attr [String] alias 别名
|
9
|
+
# @attr [String] topic 标签
|
10
|
+
# @attr [String] user_account user account
|
11
|
+
# @attr [String] topics 多个标签
|
12
|
+
# @attr [String] topic_op 配合 topics 使用
|
13
|
+
# @attr [String] extras 额外参数
|
4
14
|
class Base
|
5
|
-
attr_accessor :registration_id, :alias, :topic, :topics, :topic_op, :extras
|
15
|
+
attr_accessor :registration_id, :alias, :topic, :user_account, :topics, :topic_op, :extras
|
6
16
|
|
17
|
+
# 设置或获取附加数据
|
18
|
+
# @param [String] key
|
19
|
+
# @param [String] value
|
20
|
+
# @return [void]
|
7
21
|
def extra(key, value = nil)
|
8
22
|
unless value
|
9
23
|
@extras[key]
|
@@ -12,21 +26,35 @@ module Xiaomi
|
|
12
26
|
end
|
13
27
|
end
|
14
28
|
|
29
|
+
# 设置或获取基本数据
|
30
|
+
#
|
31
|
+
# @param [String] key 设置 :registration_id, :alias, :topic, :user_account, :topics, :topic_op, :extras
|
32
|
+
# @param [String] value
|
33
|
+
# @return [void]
|
15
34
|
def type(key, value = nil)
|
16
35
|
key = "@#{key}"
|
17
36
|
|
18
37
|
if value
|
19
|
-
instance_variable_set
|
38
|
+
instance_variable_set(key, value)
|
20
39
|
else
|
21
|
-
instance_variable_get
|
40
|
+
instance_variable_get(key)
|
22
41
|
end
|
23
42
|
end
|
24
43
|
|
25
|
-
|
44
|
+
# 转换为字典
|
45
|
+
# @return [Hash] 消息体
|
46
|
+
def to_params
|
26
47
|
hash_data = {}
|
27
48
|
instance_variables.each do |ivar|
|
28
|
-
key = ivar
|
29
|
-
|
49
|
+
key = instance_key(ivar)
|
50
|
+
|
51
|
+
key = if ios? && ios10_struct?
|
52
|
+
ios10_struct(key)
|
53
|
+
else
|
54
|
+
extra_key(key)
|
55
|
+
end
|
56
|
+
|
57
|
+
value = instance_variable_get(ivar)
|
30
58
|
|
31
59
|
next unless value
|
32
60
|
|
@@ -42,6 +70,80 @@ module Xiaomi
|
|
42
70
|
|
43
71
|
hash_data
|
44
72
|
end
|
73
|
+
|
74
|
+
# 检查是否为 iOS 10 消息体
|
75
|
+
#
|
76
|
+
# @return [Bool]
|
77
|
+
def ios10_struct?
|
78
|
+
return @ios10_struct unless @ios10_struct.nil?
|
79
|
+
|
80
|
+
@ios10_struct = false
|
81
|
+
|
82
|
+
keys = instance_variables.map {|e| instance_key(e) }
|
83
|
+
%w(title subtitle).each do |key|
|
84
|
+
return @ios10_struct = true if keys.include?(key)
|
85
|
+
end
|
86
|
+
|
87
|
+
@ios10_struct
|
88
|
+
end
|
89
|
+
|
90
|
+
# 转换 iOS 10 消息的参数
|
91
|
+
#
|
92
|
+
# 仅转换 title, subtitle, body 和 description
|
93
|
+
#
|
94
|
+
# @example
|
95
|
+
# ios10_struct('title') # => 'aps_proper_fields.title'
|
96
|
+
# ios10_struct('description') # => 'aps_proper_fields.body'
|
97
|
+
# ios10_struct('badge') # => 'badge'
|
98
|
+
#
|
99
|
+
# @param [String] key
|
100
|
+
# @return [Bool]
|
101
|
+
def ios10_struct(key)
|
102
|
+
key = 'body' if key == 'description'
|
103
|
+
return extra_key(key) unless %w(title subtitle body mutable-content).include?(key)
|
104
|
+
|
105
|
+
"aps_proper_fields.#{key}"
|
106
|
+
end
|
107
|
+
|
108
|
+
# 检测是否是 iOS 消息体
|
109
|
+
#
|
110
|
+
# @return [Bool]
|
111
|
+
def ios?
|
112
|
+
current == 'IOS'
|
113
|
+
end
|
114
|
+
|
115
|
+
# 检测是否是 Android 消息体
|
116
|
+
#
|
117
|
+
# @return [Bool]
|
118
|
+
def android?
|
119
|
+
current == 'ANDROID'
|
120
|
+
end
|
121
|
+
|
122
|
+
# 当前消息体类型
|
123
|
+
#
|
124
|
+
# @return [String] IOS/ANDROID
|
125
|
+
def current
|
126
|
+
@current ||= self.class.name.split('::')[-1].upcase
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def instance_key(var)
|
132
|
+
var.to_s.gsub('_', '-').delete('@')
|
133
|
+
end
|
134
|
+
|
135
|
+
def extra_key?(key)
|
136
|
+
%w(badge sound category).include?(key)
|
137
|
+
end
|
138
|
+
|
139
|
+
def extra_key(key)
|
140
|
+
if extra_key?(key)
|
141
|
+
key = 'sound_url' if key.include?('sound')
|
142
|
+
key = "extra.#{key}"
|
143
|
+
else
|
144
|
+
key
|
145
|
+
end
|
146
|
+
end
|
45
147
|
end
|
46
148
|
end
|
47
149
|
end
|
@@ -1,10 +1,30 @@
|
|
1
1
|
module Xiaomi
|
2
2
|
module Push
|
3
3
|
module Message
|
4
|
+
# iOS 数据消息体
|
5
|
+
# @attr [String] title 标题(仅适用于 iOS 10 以上设备)
|
6
|
+
# @attr [String] subtitle 副标题(仅适用于 iOS 10 以上设备)
|
7
|
+
# @attr [String] body 描述(仅适用于 iOS 10 以上设备)
|
8
|
+
# @attr [Integer] mutable_content 可变内容,默认 nil 不启用
|
9
|
+
# @attr [String] image 图片地址(仅适用于 iOS 10 以上设备,填写后自动启用 mutable_content)
|
10
|
+
# @attr [String] description 描述(如果设置了 title 或 subtitle 将会启用变为 {#body})
|
11
|
+
# @attr [Integer] badge 角标, 默认 1
|
12
|
+
# @attr [String] sound 声音,默认 default
|
13
|
+
# @attr [String] category iOS 8 以上可设置推送消息快速回复类别
|
4
14
|
class IOS < Base
|
5
|
-
attr_accessor :
|
15
|
+
attr_accessor :title, :subtitle, :body, :mutable_content
|
16
|
+
attr_accessor :description, :badge, :sound, :category
|
17
|
+
|
6
18
|
def initialize(**params)
|
19
|
+
@title = params[:title]
|
20
|
+
@subtitle = params[:subtitle]
|
7
21
|
@description = params[:description]
|
22
|
+
@body = params[:body] || @description
|
23
|
+
|
24
|
+
@image = params[:image]
|
25
|
+
@mutable_content = params[:mutable_content]
|
26
|
+
@mutable_content = 1 if @image
|
27
|
+
|
8
28
|
@badge = params[:badge] || 1
|
9
29
|
@sound = params[:sound] || 'default'
|
10
30
|
@category = params[:category]
|
@@ -1,20 +1,57 @@
|
|
1
1
|
module Xiaomi
|
2
2
|
module Push
|
3
3
|
module Services
|
4
|
+
# 标签类 API
|
5
|
+
#
|
6
|
+
# @attr [Client] context
|
4
7
|
class Topic
|
5
8
|
attr_reader :context
|
9
|
+
|
10
|
+
# 初始化
|
11
|
+
#
|
12
|
+
# @param [Client] context
|
6
13
|
def initialize(context)
|
7
14
|
@context = context
|
8
15
|
end
|
9
16
|
|
17
|
+
# 订阅标签
|
18
|
+
#
|
19
|
+
# 可使用 reg id 或 alias 的方式订阅标签
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# subscribe(reg_id: 'abc', topic: 'beijing')
|
23
|
+
# subscribe(alias: 'abc', topic: 'beijing')
|
24
|
+
# subscribe(alias: 'abc,def,ghi,jkl', topic: 'beijing')
|
25
|
+
#
|
26
|
+
# @param [Hash] options
|
27
|
+
# @option options [String] :reg_id 订阅 reg id,多个以逗号分割,最多 1000 个
|
28
|
+
# @option options [String] :aliases 订阅 alias,多个以逗号分割,最多 1000 个
|
29
|
+
# @option options [String] :topic 订阅名
|
30
|
+
# @option options [String] :category 分类,可选项
|
31
|
+
# @return [Hash] 小米返回数据结构
|
10
32
|
def subscribe(**options)
|
11
33
|
url, params = prepare_params(__method__.to_s, options)
|
12
|
-
@context.
|
34
|
+
@context.post(url, params)
|
13
35
|
end
|
14
36
|
|
37
|
+
# 取消订阅标签
|
38
|
+
#
|
39
|
+
# 可使用 reg id 或 alias 的方式取消订阅标签
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# unsubscribe(reg_id: 'abc', topic: 'beijing')
|
43
|
+
# unsubscribe(alias: 'abc', topic: 'beijing')
|
44
|
+
# unsubscribe(alias: 'abc,def,ghi,jkl', topic: 'beijing')
|
45
|
+
#
|
46
|
+
# @param [Hash] options
|
47
|
+
# @option options [String] :reg_id 订阅 reg id,多个以逗号分割,最多 1000 个
|
48
|
+
# @option options [String] :aliases 订阅 alias,多个以逗号分割,最多 1000 个
|
49
|
+
# @option options [String] :topic 订阅名
|
50
|
+
# @option options [String] :category 分类,可选项
|
51
|
+
# @return [Hash] 小米返回数据结构
|
15
52
|
def unsubscribe(**options)
|
16
53
|
url, params = prepare_params(__method__.to_s, options)
|
17
|
-
@context.
|
54
|
+
@context.post(url, params)
|
18
55
|
end
|
19
56
|
|
20
57
|
private
|