neocities-red 1.1.0 → 1.1.2

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +32 -32
  3. data/.github/PULL_REQUEST_TEMPLATE.md +18 -18
  4. data/.github/dependabot.yml +6 -6
  5. data/.github/workflows/ci.yml +139 -139
  6. data/.gitignore +38 -38
  7. data/.rspec +3 -3
  8. data/.rubocop.yml +43 -43
  9. data/CODE_OF_CONDUCT.md +128 -128
  10. data/CONTRIBUTING.md +83 -83
  11. data/Gemfile +12 -12
  12. data/Gemfile.lock +194 -193
  13. data/LICENSE +21 -21
  14. data/README.md +153 -153
  15. data/SECURITY.md +7 -7
  16. data/bin/neocities-red +9 -9
  17. data/ext/mkrf_conf.rb +21 -21
  18. data/lib/neocities_red/cli.rb +340 -335
  19. data/lib/neocities_red/cli_display.rb +281 -271
  20. data/lib/neocities_red/client.rb +200 -200
  21. data/lib/neocities_red/services/common/pizza.rb +107 -107
  22. data/lib/neocities_red/services/file/folder_uploader.rb +65 -65
  23. data/lib/neocities_red/services/file/list.rb +65 -65
  24. data/lib/neocities_red/services/file/remover.rb +35 -35
  25. data/lib/neocities_red/services/file/uploader.rb +45 -45
  26. data/lib/neocities_red/services/site/differencer.rb +76 -76
  27. data/lib/neocities_red/services/site/exporter.rb +43 -43
  28. data/lib/neocities_red/services/site/informer.rb +43 -43
  29. data/lib/neocities_red/services/site/pusher.rb +146 -146
  30. data/lib/neocities_red/version.rb +5 -5
  31. data/lib/neocities_red.rb +17 -17
  32. data/neocities-red.gemspec +54 -54
  33. data/spec/neocities_red/cli_display_spec.rb +139 -139
  34. data/spec/neocities_red/cli_spec.rb +68 -68
  35. data/spec/neocities_red/client_spec.rb +306 -306
  36. data/spec/neocities_red/services/common/pizza_spec.rb +26 -26
  37. data/spec/neocities_red/services/file/folder_uploader_spec.rb +64 -64
  38. data/spec/neocities_red/services/file/list_spec.rb +177 -177
  39. data/spec/neocities_red/services/file/remover_spec.rb +125 -125
  40. data/spec/neocities_red/services/file/uploader_spec.rb +158 -158
  41. data/spec/neocities_red/services/site/differencer_spec.rb +162 -162
  42. data/spec/neocities_red/services/site/exporter_spec.rb +116 -116
  43. data/spec/neocities_red/services/site/informer_spec.rb +180 -180
  44. data/spec/neocities_red/services/site/pusher_spec.rb +167 -167
  45. data/spec/spec_helper.rb +20 -20
  46. metadata +4 -10
@@ -1,200 +1,200 @@
1
- # frozen_string_literal: true
2
-
3
- if Gem.win_platform?
4
- begin
5
- require "openssl/win/root"
6
- rescue LoadError
7
- # Optional dependency that helps Windows trust store integration.
8
- end
9
- end
10
- require "json"
11
- require "pathname"
12
- require "uri"
13
- require "digest"
14
- require "pastel"
15
- require "date"
16
- require "whirly"
17
-
18
- require "faraday"
19
- require "faraday/retry"
20
- require "faraday/multipart"
21
- require "faraday/follow_redirects"
22
-
23
- module NeocitiesRed
24
- class Client
25
- API_URI = "https://neocities.org/api/"
26
-
27
- def initialize(opts = {})
28
- @uri = URI.parse API_URI
29
- @opts = opts
30
- @pastel = Pastel.new eachline: "\n"
31
- @conn = Faraday.new(@uri) do |conn|
32
- conn.options.timeout = 30
33
- conn.options.open_timeout = 10
34
- conn.adapter :net_http
35
-
36
- conn.request :multipart
37
- conn.request :url_encoded
38
-
39
- conn.request :retry,
40
- max: 3,
41
- interval: 0.3,
42
- backoff_factor: 2,
43
- retry_statuses: [429, 500, 502, 503, 504],
44
- exceptions: [
45
- Faraday::TimeoutError,
46
- Faraday::ConnectionFailed,
47
- Faraday::SSLError
48
- ]
49
-
50
- conn.response :follow_redirects
51
- end
52
-
53
- raise ArgumentError, "client requires a login (sitename/password) or an api_key" unless opts[:api_key] || (opts[:sitename] && opts[:password])
54
-
55
- if opts[:api_key]
56
- @conn.request(:authorization, "Bearer", opts[:api_key])
57
- else
58
- @conn.request(:authorization, :basic, opts[:sitename], opts[:password])
59
- end
60
- end
61
-
62
- def list(path = nil)
63
- get "list", path: path
64
- end
65
-
66
- # TODO: refactor
67
- def pull(sitename, last_pull_time = nil, last_pull_loc = nil, quiet: true)
68
- site_info = info(sitename)
69
-
70
- raise ArgumentError, site_info[:message] if site_info[:result] == "error"
71
-
72
- info_data = site_info[:info]
73
-
74
- domain =
75
- if info_data[:domain].to_s.empty?
76
- "https://#{sitename}.neocities.org/"
77
- else
78
- "https://#{info_data[:domain]}/"
79
- end
80
-
81
- # start stats
82
- success_loaded = 0
83
- start_time = Time.now
84
- curr_dir = Dir.pwd
85
-
86
- # get list of files
87
- resp = list
88
-
89
- raise ArgumentError, resp[:message] if resp[:result] == "error"
90
-
91
- # fetch each file
92
- uri_parser = URI::Parser.new
93
- resp[:files].each do |file|
94
- if file[:is_directory]
95
- FileUtils.mkdir_p file[:path].to_s
96
- else
97
- print @pastel.bold("Pulling #{file[:path]} ... ") unless quiet
98
-
99
- if last_pull_time &&
100
- last_pull_loc &&
101
- Time.parse(file[:updated_at]) <= Time.parse(last_pull_time) &&
102
- last_pull_loc == curr_dir &&
103
- File.exist?(file[:path]) # case when user deletes file
104
-
105
- # case when file hasn't been updated since last
106
- print "#{@pastel.yellow.bold 'NO NEW UPDATES'}\n" unless quiet
107
-
108
- next
109
- end
110
-
111
- pathtotry = uri_parser.escape(domain + file[:path])
112
- fileconts = @conn.get pathtotry
113
-
114
- if fileconts.status == 200
115
- print "#{@pastel.green.bold 'SUCCESS'}\n" unless quiet
116
- success_loaded += 1
117
-
118
- File.write(file[:path].to_s, fileconts.body)
119
- elsif !quiet
120
- print "#{@pastel.red.bold 'FAIL'}\n"
121
- end
122
- end
123
- end
124
-
125
- # calculate time command took
126
- total_time = Time.now - start_time
127
-
128
- # stop the spinner, if there is one
129
- Whirly.stop if quiet
130
-
131
- # display stats
132
- puts @pastel.green "\nSuccessfully fetched #{success_loaded} files in #{total_time.round(2)} seconds"
133
- end
134
-
135
- def key
136
- get "key"
137
- end
138
-
139
- def upload_hash(remote_path, sha1_hash)
140
- post "upload_hash", remote_path => sha1_hash
141
- end
142
-
143
- def upload(path, remote_path = nil, dry_run: false)
144
- path = Pathname path
145
- raise ArgumentError, "#{path} does not exist." unless path.exist?
146
-
147
- rpath = remote_path || path.basename
148
- res = upload_hash(rpath.to_s, Digest::SHA1.file(path.to_s).hexdigest)
149
-
150
- file_exists_remotely = if res[:files]
151
- res[:files][rpath.to_s.to_sym] == true || res[:files][rpath.to_s] == true
152
- else
153
- false
154
- end
155
-
156
- if file_exists_remotely
157
- {
158
- result: "error",
159
- error_type: "file_exists",
160
- message: "file already exists and matches local file, not uploading"
161
- }
162
- else
163
- return { result: "success" } if dry_run
164
-
165
- File.open(path.to_s) do |file|
166
- post "upload", rpath.to_s => Faraday::Multipart::FilePart.new(file, "text/html")
167
- end
168
- end
169
- end
170
-
171
- def delete_wrapper_with_dry_run(paths, dry_run: false)
172
- return { result: "success" } if dry_run
173
-
174
- delete(paths)
175
- end
176
-
177
- def delete(*paths)
178
- post "delete", "filenames" => paths
179
- end
180
-
181
- def info(sitename)
182
- get "info", sitename: sitename
183
- end
184
-
185
- def get(path, params = {})
186
- uri = @uri + path
187
- uri.query = URI.encode_www_form params
188
- resp = @conn.get(uri)
189
-
190
- JSON.parse resp.body, symbolize_names: true
191
- end
192
-
193
- def post(path, args = {})
194
- uri = @uri + path
195
- resp = @conn.post(uri, args)
196
-
197
- JSON.parse resp.body, symbolize_names: true
198
- end
199
- end
200
- end
1
+ # frozen_string_literal: true
2
+
3
+ if Gem.win_platform?
4
+ begin
5
+ require "openssl/win/root"
6
+ rescue LoadError
7
+ # Optional dependency that helps Windows trust store integration.
8
+ end
9
+ end
10
+ require "json"
11
+ require "pathname"
12
+ require "uri"
13
+ require "digest"
14
+ require "pastel"
15
+ require "date"
16
+ require "whirly"
17
+
18
+ require "faraday"
19
+ require "faraday/retry"
20
+ require "faraday/multipart"
21
+ require "faraday/follow_redirects"
22
+
23
+ module NeocitiesRed
24
+ class Client
25
+ API_URI = "https://neocities.org/api/"
26
+
27
+ def initialize(opts = {})
28
+ @uri = URI.parse API_URI
29
+ @opts = opts
30
+ @pastel = Pastel.new eachline: "\n"
31
+ @conn = Faraday.new(@uri) do |conn|
32
+ conn.options.timeout = 30
33
+ conn.options.open_timeout = 10
34
+ conn.adapter :net_http
35
+
36
+ conn.request :multipart
37
+ conn.request :url_encoded
38
+
39
+ conn.request :retry,
40
+ max: 3,
41
+ interval: 0.3,
42
+ backoff_factor: 2,
43
+ retry_statuses: [429, 500, 502, 503, 504],
44
+ exceptions: [
45
+ Faraday::TimeoutError,
46
+ Faraday::ConnectionFailed,
47
+ Faraday::SSLError
48
+ ]
49
+
50
+ conn.response :follow_redirects
51
+ end
52
+
53
+ raise ArgumentError, "client requires a login (sitename/password) or an api_key" unless opts[:api_key] || (opts[:sitename] && opts[:password])
54
+
55
+ if opts[:api_key]
56
+ @conn.request(:authorization, "Bearer", opts[:api_key])
57
+ else
58
+ @conn.request(:authorization, :basic, opts[:sitename], opts[:password])
59
+ end
60
+ end
61
+
62
+ def list(path = nil)
63
+ get "list", path: path
64
+ end
65
+
66
+ # TODO: refactor
67
+ def pull(sitename, last_pull_time = nil, last_pull_loc = nil, quiet: true)
68
+ site_info = info(sitename)
69
+
70
+ raise ArgumentError, site_info[:message] if site_info[:result] == "error"
71
+
72
+ info_data = site_info[:info]
73
+
74
+ domain =
75
+ if info_data[:domain].to_s.empty?
76
+ "https://#{sitename}.neocities.org/"
77
+ else
78
+ "https://#{info_data[:domain]}/"
79
+ end
80
+
81
+ # start stats
82
+ success_loaded = 0
83
+ start_time = Time.now
84
+ curr_dir = Dir.pwd
85
+
86
+ # get list of files
87
+ resp = list
88
+
89
+ raise ArgumentError, resp[:message] if resp[:result] == "error"
90
+
91
+ # fetch each file
92
+ uri_parser = URI::Parser.new
93
+ resp[:files].each do |file|
94
+ if file[:is_directory]
95
+ FileUtils.mkdir_p file[:path].to_s
96
+ else
97
+ print @pastel.bold("Pulling #{file[:path]} ... ") unless quiet
98
+
99
+ if last_pull_time &&
100
+ last_pull_loc &&
101
+ Time.parse(file[:updated_at]) <= Time.parse(last_pull_time) &&
102
+ last_pull_loc == curr_dir &&
103
+ File.exist?(file[:path]) # case when user deletes file
104
+
105
+ # case when file hasn't been updated since last
106
+ print "#{@pastel.yellow.bold 'NO NEW UPDATES'}\n" unless quiet
107
+
108
+ next
109
+ end
110
+
111
+ pathtotry = uri_parser.escape(domain + file[:path])
112
+ fileconts = @conn.get pathtotry
113
+
114
+ if fileconts.status == 200
115
+ print "#{@pastel.green.bold 'SUCCESS'}\n" unless quiet
116
+ success_loaded += 1
117
+
118
+ File.write(file[:path].to_s, fileconts.body)
119
+ elsif !quiet
120
+ print "#{@pastel.red.bold 'FAIL'}\n"
121
+ end
122
+ end
123
+ end
124
+
125
+ # calculate time command took
126
+ total_time = Time.now - start_time
127
+
128
+ # stop the spinner, if there is one
129
+ Whirly.stop if quiet
130
+
131
+ # display stats
132
+ puts @pastel.green "\nSuccessfully fetched #{success_loaded} files in #{total_time.round(2)} seconds"
133
+ end
134
+
135
+ def key
136
+ get "key"
137
+ end
138
+
139
+ def upload_hash(remote_path, sha1_hash)
140
+ post "upload_hash", remote_path => sha1_hash
141
+ end
142
+
143
+ def upload(path, remote_path = nil, dry_run: false)
144
+ path = Pathname path
145
+ raise ArgumentError, "#{path} does not exist." unless path.exist?
146
+
147
+ rpath = remote_path || path.basename
148
+ res = upload_hash(rpath.to_s, Digest::SHA1.file(path.to_s).hexdigest)
149
+
150
+ file_exists_remotely = if res[:files]
151
+ res[:files][rpath.to_s.to_sym] == true || res[:files][rpath.to_s] == true
152
+ else
153
+ false
154
+ end
155
+
156
+ if file_exists_remotely
157
+ {
158
+ result: "error",
159
+ error_type: "file_exists",
160
+ message: "file already exists and matches local file, not uploading"
161
+ }
162
+ else
163
+ return { result: "success" } if dry_run
164
+
165
+ File.open(path.to_s) do |file|
166
+ post "upload", rpath.to_s => Faraday::Multipart::FilePart.new(file, "text/html")
167
+ end
168
+ end
169
+ end
170
+
171
+ def delete_wrapper_with_dry_run(paths, dry_run: false)
172
+ return { result: "success" } if dry_run
173
+
174
+ delete(paths)
175
+ end
176
+
177
+ def delete(*paths)
178
+ post "delete", "filenames" => paths
179
+ end
180
+
181
+ def info(sitename)
182
+ get "info", sitename: sitename
183
+ end
184
+
185
+ def get(path, params = {})
186
+ uri = @uri + path
187
+ uri.query = URI.encode_www_form params
188
+ resp = @conn.get(uri)
189
+
190
+ JSON.parse resp.body, symbolize_names: true
191
+ end
192
+
193
+ def post(path, args = {})
194
+ uri = @uri + path
195
+ resp = @conn.post(uri, args)
196
+
197
+ JSON.parse resp.body, symbolize_names: true
198
+ end
199
+ end
200
+ end
@@ -1,107 +1,107 @@
1
- # frozen_string_literal: true
2
-
3
- require "pastel"
4
-
5
- module NeocitiesRed
6
- module Services
7
- module Common
8
- class Pizza
9
- EXCUSES = [
10
- "Sorry, we're fresh out of pineapple today.",
11
- "All the toppings just went rogue and are currently answering to no god.",
12
- "Our bicycle delivery guy is out today for ska band practice.",
13
- "Doughpocalypse now. Pizza's off until further notice.",
14
- "Mamma mia! We're outta the cheesa.",
15
- "The sauce of our youth ran dry. Pizza is off the menu for now.",
16
- "There was this pizza place in Portland called Lonesomes that taped burned CDs of local bands to the pizza box. It was pretty dope.",
17
- "No dough, no go, sorry joe.",
18
- "I'll be right with you after I figure out how to center a div in CSS.",
19
- "The pizza gods demand rest. Are the hunger pangs interrupting your game?",
20
- "Our pizza chef currently has the high score on the Road Kings pinball machine, you dare disturb him?",
21
- "Today's special: disappointment. Pizza unavailable.",
22
- "Our last pizza became a perpetual motion machine, left the atmosphere and is flying through the heavens.",
23
- "Ran out of oregano and optimism. See you next time.",
24
- <<~TEXT
25
- WAR AND PEACE, BY LEO TOLSTOY, BOOK ONE: 1805, CHAPTER I
26
-
27
- “Well, Prince, so Genoa and Lucca are now just family estates of the Buonapartes.
28
- But I warn you, if you don’t tell me that this means war, if you still try to defend
29
- the infamies and horrors perpetrated by that Antichrist—I really believe he is
30
- Antichrist—I will have nothing more to do with you and you are no longer my friend,
31
- no longer my ‘faithful slave,’ as you call yourself! But how do you do? I see I have
32
- frightened you—sit down and tell me all the news.”
33
-
34
- It was in July, 1805, and the speaker was the well-known Anna Pávlovna Schérer,
35
- maid of honor and favorite of the Empress Márya Fëdorovna. With these words she
36
- greeted Prince Vasíli Kurágin, a man of high rank and importance, who was the first
37
- to arrive at her reception.
38
-
39
- Anna Pávlovna had had a cough for some days. She was, as she said, suffering from
40
- la grippe; grippe being then a new word in St. Petersburg, used only by the elite.
41
-
42
- All her invitations without exception, written in French, and delivered by a
43
- scarlet-liveried footman that morning, ran as follows:
44
-
45
- “If you have nothing better to do, Count (or Prince), and if the prospect of
46
- spending an evening with a poor invalid is not too terrible, I shall be very
47
- charmed to see you tonight between 7 and 10—Annette Schérer.”
48
-
49
- “Heavens! what a virulent attack!” replied the prince, not in the least disconcerted
50
- by this reception.
51
-
52
- He had just entered, wearing an embroidered court uniform, knee breeches, and
53
- shoes, and had stars on his breast and a serene expression on his flat face.
54
-
55
- He spoke in that refined French in which our grandfathers not only spoke but
56
- thought, and with the gentle, patronizing intonation natural to a man of importance
57
- who had grown old in society and at court.
58
-
59
- He went up to Anna Pávlovna, kissed her hand, presenting to her his bald, scented,
60
- and shining head, and complacently seated himself on the sofa.
61
-
62
- “First of all, dear friend, tell me how you are. Set your friend’s mind at rest,”
63
- said he without altering his tone, beneath the politeness and affected sympathy of
64
- which indifference and even irony could be discerned.
65
-
66
- “Can one be well while suffering morally? Can one be calm in times like these if
67
- one has any feeling?” said Anna Pávlovna. “You are staying the whole evening, I hope?”
68
-
69
- “And the fete at the English ambassador’s? Today is Wednesday. I must put in an
70
- appearance there,” said the prince. “My daughter is coming for me to take me there.”
71
-
72
- “I thought today’s fete had been canceled. I confess all these festivities and
73
- fireworks are becoming wearisome.”
74
-
75
- “If they had known that you wished it, the entertainment would have been put off,”
76
- said the prince, who, like a wound-up clock, by force of habit said things he did
77
- not even wish to be believed.
78
-
79
- “Don’t tease! Well, and what has been decided about Novosíltsev’s dispatch?
80
- You know everything.”
81
-
82
- “What can one say about it?” replied the prince in a cold, listless tone.
83
- “What has been decided? They have decided that Buonaparte has burnt his boats,
84
- and I believe that we are ready to burn ours.”
85
-
86
- Prince Vasíli always spoke languidly, like an actor repeating a stale part.
87
- Anna Pávlovna Schérer on the contrary, despite her forty years, overflowed with
88
- animation and impulsiveness.
89
-
90
- To be an enthusiast had become her social vocation and, sometimes even when she
91
- did not feel like it, she became enthusiastic in order not to disappoint the
92
- expectations of those who knew her.
93
-
94
- The subdued smile which, though it did not suit her faded features, always played
95
- round her lips expressed, as in a spoiled child, a continual consciousness of her
96
- charming defect, which she neither wished, nor could, nor considered it necessary,
97
- to correct.
98
- TEXT
99
- ].freeze
100
-
101
- def make_order
102
- Pastel.new(enabled: true).bright_red(EXCUSES.sample)
103
- end
104
- end
105
- end
106
- end
107
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module NeocitiesRed
6
+ module Services
7
+ module Common
8
+ class Pizza
9
+ EXCUSES = [
10
+ "Sorry, we're fresh out of pineapple today.",
11
+ "All the toppings just went rogue and are currently answering to no god.",
12
+ "Our bicycle delivery guy is out today for ska band practice.",
13
+ "Doughpocalypse now. Pizza's off until further notice.",
14
+ "Mamma mia! We're outta the cheesa.",
15
+ "The sauce of our youth ran dry. Pizza is off the menu for now.",
16
+ "There was this pizza place in Portland called Lonesomes that taped burned CDs of local bands to the pizza box. It was pretty dope.",
17
+ "No dough, no go, sorry joe.",
18
+ "I'll be right with you after I figure out how to center a div in CSS.",
19
+ "The pizza gods demand rest. Are the hunger pangs interrupting your game?",
20
+ "Our pizza chef currently has the high score on the Road Kings pinball machine, you dare disturb him?",
21
+ "Today's special: disappointment. Pizza unavailable.",
22
+ "Our last pizza became a perpetual motion machine, left the atmosphere and is flying through the heavens.",
23
+ "Ran out of oregano and optimism. See you next time.",
24
+ <<~TEXT
25
+ WAR AND PEACE, BY LEO TOLSTOY, BOOK ONE: 1805, CHAPTER I
26
+
27
+ “Well, Prince, so Genoa and Lucca are now just family estates of the Buonapartes.
28
+ But I warn you, if you don’t tell me that this means war, if you still try to defend
29
+ the infamies and horrors perpetrated by that Antichrist—I really believe he is
30
+ Antichrist—I will have nothing more to do with you and you are no longer my friend,
31
+ no longer my ‘faithful slave,’ as you call yourself! But how do you do? I see I have
32
+ frightened you—sit down and tell me all the news.”
33
+
34
+ It was in July, 1805, and the speaker was the well-known Anna Pávlovna Schérer,
35
+ maid of honor and favorite of the Empress Márya Fëdorovna. With these words she
36
+ greeted Prince Vasíli Kurágin, a man of high rank and importance, who was the first
37
+ to arrive at her reception.
38
+
39
+ Anna Pávlovna had had a cough for some days. She was, as she said, suffering from
40
+ la grippe; grippe being then a new word in St. Petersburg, used only by the elite.
41
+
42
+ All her invitations without exception, written in French, and delivered by a
43
+ scarlet-liveried footman that morning, ran as follows:
44
+
45
+ “If you have nothing better to do, Count (or Prince), and if the prospect of
46
+ spending an evening with a poor invalid is not too terrible, I shall be very
47
+ charmed to see you tonight between 7 and 10—Annette Schérer.”
48
+
49
+ “Heavens! what a virulent attack!” replied the prince, not in the least disconcerted
50
+ by this reception.
51
+
52
+ He had just entered, wearing an embroidered court uniform, knee breeches, and
53
+ shoes, and had stars on his breast and a serene expression on his flat face.
54
+
55
+ He spoke in that refined French in which our grandfathers not only spoke but
56
+ thought, and with the gentle, patronizing intonation natural to a man of importance
57
+ who had grown old in society and at court.
58
+
59
+ He went up to Anna Pávlovna, kissed her hand, presenting to her his bald, scented,
60
+ and shining head, and complacently seated himself on the sofa.
61
+
62
+ “First of all, dear friend, tell me how you are. Set your friend’s mind at rest,”
63
+ said he without altering his tone, beneath the politeness and affected sympathy of
64
+ which indifference and even irony could be discerned.
65
+
66
+ “Can one be well while suffering morally? Can one be calm in times like these if
67
+ one has any feeling?” said Anna Pávlovna. “You are staying the whole evening, I hope?”
68
+
69
+ “And the fete at the English ambassador’s? Today is Wednesday. I must put in an
70
+ appearance there,” said the prince. “My daughter is coming for me to take me there.”
71
+
72
+ “I thought today’s fete had been canceled. I confess all these festivities and
73
+ fireworks are becoming wearisome.”
74
+
75
+ “If they had known that you wished it, the entertainment would have been put off,”
76
+ said the prince, who, like a wound-up clock, by force of habit said things he did
77
+ not even wish to be believed.
78
+
79
+ “Don’t tease! Well, and what has been decided about Novosíltsev’s dispatch?
80
+ You know everything.”
81
+
82
+ “What can one say about it?” replied the prince in a cold, listless tone.
83
+ “What has been decided? They have decided that Buonaparte has burnt his boats,
84
+ and I believe that we are ready to burn ours.”
85
+
86
+ Prince Vasíli always spoke languidly, like an actor repeating a stale part.
87
+ Anna Pávlovna Schérer on the contrary, despite her forty years, overflowed with
88
+ animation and impulsiveness.
89
+
90
+ To be an enthusiast had become her social vocation and, sometimes even when she
91
+ did not feel like it, she became enthusiastic in order not to disappoint the
92
+ expectations of those who knew her.
93
+
94
+ The subdued smile which, though it did not suit her faded features, always played
95
+ round her lips expressed, as in a spoiled child, a continual consciousness of her
96
+ charming defect, which she neither wished, nor could, nor considered it necessary,
97
+ to correct.
98
+ TEXT
99
+ ].freeze
100
+
101
+ def make_order
102
+ Pastel.new(enabled: true).bright_red(EXCUSES.sample)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end