simple_tweet 2.2.0 → 3.0.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
  SHA256:
3
- metadata.gz: 7e8b7b5eef07f8a7108285eeb67aa7708fd0f810b49c497e4d2a2e7e988f671d
4
- data.tar.gz: f0501a28e4072c89f67944a7982394fd21ebd3fbc0e852dd18b5d3eb98fc0d97
3
+ metadata.gz: cb994313a55f3a80782b1c73c983a7551fa2a394f49aa5149efdf59372d20d40
4
+ data.tar.gz: 2dff9773d979e4f907b1ad37072adc59d30d13c64d73ed26a004032a50cc2456
5
5
  SHA512:
6
- metadata.gz: 38863d94493b2b4a71f0306327d7db02785e2118d852b5dd5508e642e9bbc0d8d453104ab08853da9b2ded125d49e782afa99913fe16f09726c0da5acad77d53
7
- data.tar.gz: 7e8e2ad9278bacef086651331964f40c1020257339e3599128903081cc5123ec40904597c6568a51bd10c7a10597ec16d52fab2a3bdc5c58aeadc97726ec7d48
6
+ metadata.gz: e5425884322fcb27da54bf4747c709ad412c9479daf0fd0b040209dbbec8be6f5d930505b4a72c85dabbef8a55b5817b5f5d83f7be2a8255d4e2aaa9253ec4e3
7
+ data.tar.gz: 31fb1439304050a6038b19ce25520795eb3588ebcdfe29b75b1249adbbdfff932f79833c131ef3492ff784599f2e4e3301222f7885784d60de2a634652d6cbef
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.6
2
+ TargetRubyVersion: 3.0
3
3
 
4
4
  Style/StringLiterals:
5
5
  Enabled: true
@@ -13,7 +13,7 @@ Layout/LineLength:
13
13
  Max: 120
14
14
 
15
15
  Metrics/ClassLength:
16
- Max: 120
16
+ Max: 150
17
17
 
18
18
  Metrics/MethodLength:
19
19
  Max: 30
data/Gemfile CHANGED
@@ -6,7 +6,5 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
-
10
9
  gem "rspec", "~> 3.0"
11
-
12
- gem "rubocop", "~> 1.21"
10
+ gem "rubocop", "~> 1.56"
data/Gemfile.lock ADDED
@@ -0,0 +1,77 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ simple_tweet (3.0.0)
5
+ multipart-post (>= 2.2.3)
6
+ oauth (~> 1.1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ast (2.4.2)
12
+ base64 (0.1.1)
13
+ diff-lcs (1.5.0)
14
+ hashie (5.0.0)
15
+ json (2.6.3)
16
+ language_server-protocol (3.17.0.3)
17
+ multipart-post (2.3.0)
18
+ oauth (1.1.0)
19
+ oauth-tty (~> 1.0, >= 1.0.1)
20
+ snaky_hash (~> 2.0)
21
+ version_gem (~> 1.1)
22
+ oauth-tty (1.0.5)
23
+ version_gem (~> 1.1, >= 1.1.1)
24
+ parallel (1.23.0)
25
+ parser (3.2.2.3)
26
+ ast (~> 2.4.1)
27
+ racc
28
+ racc (1.7.1)
29
+ rainbow (3.1.1)
30
+ rake (13.0.6)
31
+ regexp_parser (2.8.1)
32
+ rexml (3.2.6)
33
+ rspec (3.12.0)
34
+ rspec-core (~> 3.12.0)
35
+ rspec-expectations (~> 3.12.0)
36
+ rspec-mocks (~> 3.12.0)
37
+ rspec-core (3.12.1)
38
+ rspec-support (~> 3.12.0)
39
+ rspec-expectations (3.12.2)
40
+ diff-lcs (>= 1.2.0, < 2.0)
41
+ rspec-support (~> 3.12.0)
42
+ rspec-mocks (3.12.3)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.12.0)
45
+ rspec-support (3.12.0)
46
+ rubocop (1.56.2)
47
+ base64 (~> 0.1.1)
48
+ json (~> 2.3)
49
+ language_server-protocol (>= 3.17.0)
50
+ parallel (~> 1.10)
51
+ parser (>= 3.2.2.3)
52
+ rainbow (>= 2.2.2, < 4.0)
53
+ regexp_parser (>= 1.8, < 3.0)
54
+ rexml (>= 3.2.5, < 4.0)
55
+ rubocop-ast (>= 1.28.1, < 2.0)
56
+ ruby-progressbar (~> 1.7)
57
+ unicode-display_width (>= 2.4.0, < 3.0)
58
+ rubocop-ast (1.29.0)
59
+ parser (>= 3.2.1.0)
60
+ ruby-progressbar (1.13.0)
61
+ snaky_hash (2.0.1)
62
+ hashie
63
+ version_gem (~> 1.1, >= 1.1.1)
64
+ unicode-display_width (2.4.2)
65
+ version_gem (1.1.3)
66
+
67
+ PLATFORMS
68
+ arm64-darwin-21
69
+
70
+ DEPENDENCIES
71
+ rake (~> 13.0)
72
+ rspec (~> 3.0)
73
+ rubocop (~> 1.56)
74
+ simple_tweet!
75
+
76
+ BUNDLED WITH
77
+ 2.3.24
@@ -1,18 +1,178 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
4
+ require "cgi"
5
+ require "oauth"
6
+ require "net/http/post/multipart"
2
7
 
3
8
  module SimpleTweet
4
9
  module V2
5
- # mediaのuploadはapi 1.1しか用意されていないため、それを使う。
6
- class Client < V1::Client
10
+ # Twitte API v2を叩くクライアント
11
+ class Client
12
+ TW_API_ORIGIN = "https://api.twitter.com"
13
+ TW_UPLOAD_ORIGIN = "https://upload.twitter.com"
14
+ TW_MEDIA_UPLOAD_PATH = "/1.1/media/upload.json"
15
+ TW_METADATA_CREATE_PATH = "/1.1/media/metadata/create.json"
7
16
  TW_TWEET_PATH = "/2/tweets"
8
- UA = "SimpleTweet/#{SimpleTweet::VERSION}"
17
+ UA = "SimpleTweet/#{SimpleTweet::VERSION}".freeze
18
+ APPEND_PER = 5 * (1 << 20)
19
+
20
+ def initialize(consumer_key:, consumer_secret:, access_token:, access_token_secret:, max_append_retry: 3)
21
+ @consumer_key_ = consumer_key
22
+ @consumer_secret_ = consumer_secret
23
+ @access_token_ = access_token
24
+ @access_token_secret_ = access_token_secret
25
+ @max_append_retry_ = max_append_retry
26
+ end
9
27
 
28
+ # https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/migrate
10
29
  def tweet(message:, media_ids: [])
11
30
  json = { text: message }
12
31
  json[:media] = { media_ids: media_ids } unless media_ids.empty?
13
32
  header = { "User-Agent": UA, "content-type": "application/json" }
14
33
  access_token.post(TW_TWEET_PATH, json.to_json, header)
15
34
  end
35
+
36
+ def tweet_with_media(message:, media_type:, media:, alt_text: nil)
37
+ media_ids = upload_media(media_type: media_type, media: media)
38
+ unless alt_text.nil?
39
+ media_ids.each do |media_id|
40
+ create_media_metadata(media_id: media_id, alt_text: alt_text)
41
+ end
42
+ end
43
+ tweet(message: message, media_ids: media_ids)
44
+ end
45
+
46
+ private
47
+
48
+ def access_token(site: TW_API_ORIGIN)
49
+ consumer = ::OAuth::Consumer.new(@consumer_key_, @consumer_secret_, site: site)
50
+ ::OAuth::AccessToken.new(consumer, @access_token_, @access_token_secret_)
51
+ end
52
+
53
+ def request(req)
54
+ @client ||= access_token(site: TW_UPLOAD_ORIGIN)
55
+ @client.sign! req
56
+
57
+ url = ::URI.parse(TW_UPLOAD_ORIGIN + TW_MEDIA_UPLOAD_PATH)
58
+ https = ::Net::HTTP.new(url.host, url.port)
59
+ https.use_ssl = true
60
+
61
+ https.start do |http|
62
+ http.request req
63
+ end
64
+ end
65
+
66
+ # https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload
67
+ ## maybe todo: multiple image
68
+ # ここはv1のAPIを叩いている。
69
+ def upload_media(media_type:, media:)
70
+ return upload_video(video: media) if media_type == "video/mp4"
71
+
72
+ req = ::Net::HTTP::Post::Multipart.new(
73
+ TW_MEDIA_UPLOAD_PATH,
74
+ media: ::UploadIO.new(media, media_type),
75
+ media_category: "tweet_image"
76
+ )
77
+ res = ::JSON.parse(request(req).body)
78
+ [res["media_id_string"]]
79
+ end
80
+
81
+ def init(video:)
82
+ init_req = ::Net::HTTP::Post::Multipart.new(
83
+ TW_MEDIA_UPLOAD_PATH,
84
+ command: "INIT",
85
+ total_bytes: video.size,
86
+ media_type: "video/mp4"
87
+ )
88
+ init_res = request(init_req)
89
+ raise UploadMediaError.new("init failed", response: init_res) unless init_res.code == "202"
90
+
91
+ ::JSON.parse(init_res.body)
92
+ end
93
+
94
+ def append(video:, media_id:, index:, retry_count: 0)
95
+ append_req = ::Net::HTTP::Post::Multipart.new(
96
+ TW_MEDIA_UPLOAD_PATH,
97
+ command: "APPEND",
98
+ media_id: media_id,
99
+ media: video.read(APPEND_PER),
100
+ segment_index: index
101
+ )
102
+ res = request(append_req)
103
+ return if res.code == "204"
104
+ raise UploadMediaError.new("append failed", response: res) unless retry_count <= @max_append_retry_
105
+
106
+ append(video: video, media_id: media_id, index: index, retry_count: retry_count + 1)
107
+ end
108
+
109
+ def finalize(media_id:)
110
+ finalize_req = ::Net::HTTP::Post::Multipart.new(
111
+ TW_MEDIA_UPLOAD_PATH,
112
+ command: "FINALIZE",
113
+ media_id: media_id
114
+ )
115
+ finalize_res = request(finalize_req)
116
+ raise UploadMediaError.new("finalize failed", response: finalize_res) unless finalize_res.code == "201"
117
+
118
+ ::JSON.parse(finalize_res.body)
119
+ end
120
+
121
+ def status(media_id:)
122
+ status_req = ::Net::HTTP::Post::Multipart.new(
123
+ TW_MEDIA_UPLOAD_PATH,
124
+ command: "STATUS",
125
+ media_id: media_id
126
+ )
127
+ status_res = request(status_req)
128
+ raise UploadMediaError.new("status failed", response: status_res) unless status_res.code == "200"
129
+
130
+ ::JSON.parse(status_res.body)
131
+ end
132
+
133
+ # https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload-init
134
+ def upload_video(video:)
135
+ init_res = init(video: video)
136
+ media_id = init_res["media_id_string"]
137
+
138
+ chunks_needed = (video.size - 1) / APPEND_PER + 1
139
+ chunks_needed.times do |i|
140
+ append(video: video, media_id: media_id, index: i)
141
+ end
142
+
143
+ finalize_res = finalize(media_id: media_id)
144
+
145
+ if finalize_res["processing_info"]
146
+ retry_after = finalize_res["processing_info"]["check_after_secs"] || 5
147
+ loop do
148
+ sleep retry_after
149
+
150
+ status_res = status(media_id: media_id)
151
+ raise UploadMediaError if status_res["processing_info"].nil?
152
+ break if status_res["processing_info"]["state"] == "succeeded"
153
+
154
+ if status_res["processing_info"]["state"] == "in_progress"
155
+ retry_after = status_res["processing_info"]["check_after_secs"] || 5
156
+ next
157
+ end
158
+
159
+ # status_res_json["processing_info"]["state"] == "failed"
160
+ raise UploadMediaError
161
+ end
162
+ end
163
+
164
+ [media_id]
165
+ end
166
+
167
+ def create_media_metadata(media_id:, alt_text:)
168
+ header = { "content-type": "application/json; charset=UTF-8" }
169
+ req = ::Net::HTTP::Post.new(TW_METADATA_CREATE_PATH, header)
170
+ req.body = { media_id: media_id, alt_text: { text: alt_text } }.to_json
171
+ res = request(req)
172
+ raise UploadMediaError.new("create_media_metadata failed", response: res) if res.code != "200"
173
+
174
+ res
175
+ end
16
176
  end
17
177
  end
18
178
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleTweet
4
- VERSION = "2.2.0"
4
+ VERSION = "3.0.0"
5
5
  end
data/lib/simple_tweet.rb CHANGED
@@ -1,132 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "simple_tweet/version"
4
- require_relative "simple_tweet/v1_client"
5
4
  require_relative "simple_tweet/v2_client"
6
- require "json"
7
5
 
8
6
  module SimpleTweet
9
7
  class Error < ::StandardError; end
10
- class UploadMediaError < Error; end
11
8
 
12
- # Twitter::Tweetに近いinterfaceを提供する。
13
- ## 使ってない部分のhashの中身のhashとかは正規化されてないので、必要になったら足す必要がある。
14
- ### todo: entities, media, ...
15
- ## 逆に、本体とuserのところだけHashから変換している。
16
- Tweet = ::Struct.new(
17
- :created_at,
18
- :id,
19
- :id_str,
20
- :text,
21
- :truncated,
22
- :entities,
23
- :source,
24
- :in_reply_to_status_id,
25
- :in_reply_to_status_id_str,
26
- :in_reply_to_user_id,
27
- :in_reply_to_user_id_str,
28
- :in_reply_to_screen_name,
29
- :user,
30
- :geo,
31
- :coordinates,
32
- :place,
33
- :contributors,
34
- :is_quote_status,
35
- :retweet_count,
36
- :favorite_count,
37
- :favorited,
38
- :retweeted,
39
- :lang,
40
- :extended_entities,
41
- :possibly_sensitive,
42
- :quoted_status_id,
43
- :quoted_status_id_str,
44
- :quoted_status,
45
- keyword_init: true
46
- )
47
- User = ::Struct.new(
48
- :id,
49
- :id_str,
50
- :name,
51
- :screen_name,
52
- :location,
53
- :description,
54
- :url,
55
- :entities,
56
- :protected,
57
- :followers_count,
58
- :friends_count,
59
- :listed_count,
60
- :created_at,
61
- :favourites_count,
62
- :utc_offset,
63
- :time_zone,
64
- :geo_enabled,
65
- :verified,
66
- :statuses_count,
67
- :lang,
68
- :contributors_enabled,
69
- :is_translator,
70
- :is_translation_enabled,
71
- :profile_background_color,
72
- :profile_background_image_url,
73
- :profile_background_image_url_https,
74
- :profile_background_tile,
75
- :profile_image_url,
76
- :profile_image_url_https,
77
- :profile_banner_url,
78
- :profile_link_color,
79
- :profile_sidebar_border_color,
80
- :profile_sidebar_fill_color,
81
- :profile_text_color,
82
- :profile_use_background_image,
83
- :has_extended_profile,
84
- :default_profile,
85
- :default_profile_image,
86
- :following,
87
- :follow_request_sent,
88
- :notifications,
89
- :translator_type,
90
- :withheld_in_countries,
91
- keyword_init: true
92
- )
9
+ # UploadMediaError is
10
+ class UploadMediaError < Error
11
+ attr_reader :response
93
12
 
94
- # Tweet is like Twitter::Tweet
95
- class Tweet
96
- def self.from_response(response)
97
- return nil unless response.code == "200"
98
-
99
- res = ::JSON.parse(response.body)
100
- tw = Tweet.new(**res)
101
- tw.created_at = ::Time.parse(tw.created_at).utc
102
- tw.user = User.new(**res["user"])
103
- tw
104
- end
105
-
106
- def uri
107
- "https://twitter.com/#{user.screen_name}/status/#{id}"
108
- end
109
- alias url uri
110
-
111
- def to_h
112
- super.map do |k, v|
113
- if k == :user
114
- [k.to_s, v.to_h]
115
- else
116
- [k.to_s, v]
117
- end
118
- end.to_h
119
- end
120
- end
121
-
122
- # User is
123
- class User
124
- def protected?
125
- protected
126
- end
127
-
128
- def to_h
129
- super.transform_keys(&:to_s)
13
+ def initialize(message = nil, response: nil)
14
+ super(message)
15
+ @response = response
130
16
  end
131
17
  end
132
18
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_tweet
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kugayama Nana
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-06-15 00:00:00.000000000 Z
11
+ date: 2023-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multipart-post
@@ -49,11 +49,11 @@ files:
49
49
  - ".rubocop.yml"
50
50
  - CODE_OF_CONDUCT.md
51
51
  - Gemfile
52
+ - Gemfile.lock
52
53
  - LICENSE.txt
53
54
  - README.md
54
55
  - Rakefile
55
56
  - lib/simple_tweet.rb
56
- - lib/simple_tweet/v1_client.rb
57
57
  - lib/simple_tweet/v2_client.rb
58
58
  - lib/simple_tweet/version.rb
59
59
  - sig/simple_tweet.rbs
@@ -1,156 +0,0 @@
1
- require "cgi"
2
- require "oauth"
3
- require "json"
4
- require "net/http/post/multipart"
5
-
6
- module SimpleTweet
7
- module V1
8
- # Client provides only tweet
9
- class Client
10
- TW_API_ORIGIN = "https://api.twitter.com"
11
- TW_UPLOAD_ORIGIN = "https://upload.twitter.com"
12
- TW_MEDIA_UPLOAD_PATH = "/1.1/media/upload.json"
13
- APPEND_PER = 5 * (1 << 20)
14
-
15
- def initialize(consumer_key:, consumer_secret:, access_token:, access_token_secret:, max_append_retry: 3)
16
- @consumer_key_ = consumer_key
17
- @consumer_secret_ = consumer_secret
18
- @access_token_ = access_token
19
- @access_token_secret_ = access_token_secret
20
- @max_append_retry_ = max_append_retry
21
- end
22
-
23
- # https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-update
24
- def tweet(message:, media_ids: [])
25
- path = "/1.1/statuses/update.json?status=#{::CGI.escape(message)}"
26
- path += "&media_ids=#{media_ids.join(",")}" unless media_ids.empty?
27
- Tweet.from_response(access_token.post(path))
28
- end
29
-
30
- # media_type is mime_type
31
- def tweet_with_media(message:, media_type:, media:)
32
- media_ids = upload_media(media_type: media_type, media: media)
33
- tweet(message: message, media_ids: media_ids)
34
- end
35
-
36
- private
37
-
38
- def access_token(site: TW_API_ORIGIN)
39
- consumer = ::OAuth::Consumer.new(@consumer_key_, @consumer_secret_, site: site)
40
- ::OAuth::AccessToken.new(consumer, @access_token_, @access_token_secret_)
41
- end
42
-
43
- def request(req)
44
- @client ||= access_token(site: TW_UPLOAD_ORIGIN)
45
- @client.sign! req
46
-
47
- url = ::URI.parse(TW_UPLOAD_ORIGIN + TW_MEDIA_UPLOAD_PATH)
48
- https = ::Net::HTTP.new(url.host, url.port)
49
- https.use_ssl = true
50
-
51
- https.start do |http|
52
- http.request req
53
- end
54
- end
55
-
56
- # https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload
57
- ## maybe todo: multiple image
58
- def upload_media(media_type:, media:)
59
- return upload_video(video: media) if media_type == "video/mp4"
60
-
61
- req = ::Net::HTTP::Post::Multipart.new(
62
- TW_MEDIA_UPLOAD_PATH,
63
- media: ::UploadIO.new(media, media_type),
64
- media_category: "tweet_image"
65
- )
66
- res = ::JSON.parse(request(req).body)
67
- [res["media_id_string"]]
68
- end
69
-
70
- def init(video:)
71
- init_req = ::Net::HTTP::Post::Multipart.new(
72
- TW_MEDIA_UPLOAD_PATH,
73
- command: "INIT",
74
- total_bytes: video.size,
75
- media_type: "video/mp4"
76
- )
77
- init_res = request(init_req)
78
- raise UploadMediaError unless init_res.code == "202"
79
-
80
- ::JSON.parse(init_res.body)
81
- end
82
-
83
- def append(video:, media_id:, index:, retry_count: 0)
84
- append_req = ::Net::HTTP::Post::Multipart.new(
85
- TW_MEDIA_UPLOAD_PATH,
86
- command: "APPEND",
87
- media_id: media_id,
88
- media: video.read(APPEND_PER),
89
- segment_index: index
90
- )
91
- return if request(append_req).code == "204"
92
- raise UploadMediaError unless retry_count <= @max_append_retry_
93
-
94
- append(video: video, media_id: media_id, index: index, retry_count: retry_count + 1)
95
- end
96
-
97
- def finalize(media_id:)
98
- finalize_req = ::Net::HTTP::Post::Multipart.new(
99
- TW_MEDIA_UPLOAD_PATH,
100
- command: "FINALIZE",
101
- media_id: media_id
102
- )
103
- finalize_res = request(finalize_req)
104
- raise UploadMediaError unless finalize_res.code == "201"
105
-
106
- ::JSON.parse(finalize_res.body)
107
- end
108
-
109
- def status(media_id:)
110
- status_req = ::Net::HTTP::Post::Multipart.new(
111
- TW_MEDIA_UPLOAD_PATH,
112
- command: "STATUS",
113
- media_id: media_id
114
- )
115
- status_res = request(status_req)
116
- raise UploadMediaError unless status_res.code == "200"
117
-
118
- ::JSON.parse(status_res.body)
119
- end
120
-
121
- # https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload-init
122
- def upload_video(video:)
123
- init_res = init(video: video)
124
- media_id = init_res["media_id_string"]
125
-
126
- chunks_needed = (video.size - 1) / APPEND_PER + 1
127
- chunks_needed.times do |i|
128
- append(video: video, media_id: media_id, index: i)
129
- end
130
-
131
- finalize_res = finalize(media_id: media_id)
132
-
133
- if finalize_res["processing_info"]
134
- retry_after = finalize_res["processing_info"]["check_after_secs"] || 5
135
- loop do
136
- sleep retry_after
137
-
138
- status_res = status(media_id: media_id)
139
- raise UploadMediaError if status_res["processing_info"].nil?
140
- break if status_res["processing_info"]["state"] == "succeeded"
141
-
142
- if status_res["processing_info"]["state"] == "in_progress"
143
- retry_after = status_res["processing_info"]["check_after_secs"] || 5
144
- next
145
- end
146
-
147
- # status_res_json["processing_info"]["state"] == "failed"
148
- raise UploadMediaError
149
- end
150
- end
151
-
152
- [media_id]
153
- end
154
- end
155
- end
156
- end