inat-channel 0.8.2 → 0.9.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.
@@ -4,56 +4,21 @@ require 'sanitize'
4
4
 
5
5
  require_relative 'icons'
6
6
 
7
- module INatChannel
8
- FORMATS = {
9
- date: '%Y.%m.%d',
10
- time: '%H:%M %Z',
11
- datetime: '%Y.%m.%d %H:%M %Z',
12
- location: :DMS, # or :decimal
13
- zoom: 12,
14
- description_limit: 512
15
- }
16
- end
17
-
18
7
  class Date
19
8
  def icon
20
- INatChannel::Icons::ICONS[:calendar]
21
- end
22
- def to_s
23
- fmt = INatChannel::FORMATS[:date]
24
- if fmt
25
- self.strftime fmt
26
- else
27
- super
28
- end
9
+ IC::ICONS[:calendar]
29
10
  end
30
11
  end
31
12
 
32
13
  class Time
33
14
  def icon
34
- INatChannel::Icons::clock_icon self
35
- end
36
- def to_s
37
- fmt = INatChannel::FORMATS[:time]
38
- if fmt
39
- self.strftime fmt
40
- else
41
- super
42
- end
15
+ IC::clock_icon self
43
16
  end
44
17
  end
45
18
 
46
19
  class DateTime
47
20
  def icon
48
- INatChannel::Icons::ICONS[:calendar]
49
- end
50
- def to_s
51
- fmt = INatChannel::FORMATS[:datetime]
52
- if fmt
53
- self.strftime fmt
54
- else
55
- super
56
- end
21
+ IC::ICONS[:calendar]
57
22
  end
58
23
  end
59
24
 
@@ -93,9 +58,9 @@ class Array
93
58
  end
94
59
  end
95
60
 
96
- Observation = Struct::new :taxon, :id, :uuid, :url, :user, :datetime, :places, :place_guess, :description, :location, keyword_init: true do
61
+ Observation = Data::define :taxon, :id, :uuid, :url, :user, :datetime, :places, :place_guess, :description, :location do
97
62
  def icon
98
- INatChannel::Icons::ICONS[:observation]
63
+ IC::ICONS[:observation]
99
64
  end
100
65
  def date
101
66
  datetime.to_date
@@ -105,9 +70,9 @@ Observation = Struct::new :taxon, :id, :uuid, :url, :user, :datetime, :places, :
105
70
  end
106
71
  end
107
72
 
108
- Taxon = Struct::new :scientific_name, :common_name, :id, :ancestors, keyword_init: true do
73
+ Taxon = Data::define :scientific_name, :common_name, :id, :ancestors do
109
74
  def icon
110
- INatChannel::Icons::ancestors_icon ancestors.map(&:id)
75
+ IC::ancestors_icon ancestors.map(&:id)
111
76
  end
112
77
  def title
113
78
  if common_name && !common_name.empty?
@@ -124,15 +89,15 @@ Taxon = Struct::new :scientific_name, :common_name, :id, :ancestors, keyword_ini
124
89
  end
125
90
  end
126
91
 
127
- Ancestor = Struct::new :scientific_name, :id, keyword_init: true do
92
+ Ancestor = Data::define :scientific_name, :id do
128
93
  def to_tag
129
94
  scientific_name.to_tag
130
95
  end
131
96
  end
132
97
 
133
- Place = Struct::new :ids, :text, :link, :tag, keyword_init: true do
98
+ Place = Data::define :text, :link, :tag do
134
99
  def icon
135
- INatChannel::Icons::ICONS[:place]
100
+ IC::ICONS[:place]
136
101
  end
137
102
  def to_tag
138
103
  tag&.to_tag
@@ -145,9 +110,9 @@ Place = Struct::new :ids, :text, :link, :tag, keyword_init: true do
145
110
  end
146
111
  end
147
112
 
148
- User = Struct::new :id, :login, :name, keyword_init: true do
113
+ User = Data::define :id, :login, :name do
149
114
  def icon
150
- INatChannel::Icons::ICONS[:user]
115
+ IC::ICONS[:user]
151
116
  end
152
117
  def title
153
118
  if name && !name.empty?
@@ -161,28 +126,32 @@ User = Struct::new :id, :login, :name, keyword_init: true do
161
126
  end
162
127
  end
163
128
 
164
- SANITIZE_HTML_CONFIG = {
165
- elements: [ 'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del', 'a', 'code', 'pre', 'tg-spoiler', 'blockquote' ],
166
- attributes: { 'a' => [ 'href' ] },
167
- protocols: { 'a' => { 'href' => [ 'http', 'https', 'mailto', 'tg' ] } },
168
- remove_contents: [ 'script', 'style' ]
169
- }
129
+ module IC
130
+
131
+ SANITIZE_HTML_CONFIG = {
132
+ elements: [ 'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del', 'a', 'code', 'pre', 'tg-spoiler', 'blockquote' ],
133
+ attributes: { 'a' => [ 'href' ] },
134
+ protocols: { 'a' => { 'href' => [ 'http', 'https', 'mailto', 'tg' ] } },
135
+ remove_contents: [ 'script', 'style' ]
136
+ }
137
+
138
+ SANITIZE_TEXT_CONFIG = {
139
+ elements: [],
140
+ remove_contents: [ 'script', 'style' ]
141
+ }
170
142
 
171
- SANITIZE_TEXT_CONFIG = {
172
- elements: [],
173
- remove_contents: [ 'script', 'style' ]
174
- }
143
+ end
175
144
 
176
- Description = Struct::new :value, keyword_init: true do
145
+ Description = Data::define :value do
177
146
  def icon
178
- INatChannel::Icons::ICONS[:description]
147
+ IC::ICONS[:description]
179
148
  end
180
149
  def text
181
- Sanitize.fragment(value, SANITIZE_TEXT_CONFIG).limit(FORMATS[:description_limit])
150
+ Sanitize.fragment(value, IC::SANITIZE_TEXT_CONFIG).limit(IC::CONFIG.dig(:tg_bot, :desc_limit))
182
151
  end
183
152
  def html
184
- sanitized = Sanitize.fragment value, SANITIZE_HTML_CONFIG
185
- if sanitized.length > FORMATS[:description_limit]
153
+ sanitized = Sanitize.fragment value, IC::SANITIZE_HTML_CONFIG
154
+ if sanitized.length > IC::CONFIG.dig(:tg_bot, :desc_limit)
186
155
  # В отличие от простого текста, обрезка HTML требует куда более изощренной логики, что неоправданно
187
156
  text
188
157
  else
@@ -191,36 +160,39 @@ Description = Struct::new :value, keyword_init: true do
191
160
  end
192
161
  end
193
162
 
194
- Location = Struct::new :lat, :lng, keyword_init: true do
163
+ Location = Data::define :lat, :lng do
195
164
  def icon
196
- INatChannel::Icons::ICONS[:location]
165
+ IC::ICONS[:location]
197
166
  end
198
- def title
199
- lat_dir = lat >= 0 ? 'N' : 'S'
200
- lng_dir = lng >= 0 ? 'E' : 'W'
167
+ def dms
168
+ lat_dir = lat >= 0 ? "N" : "S"
169
+ lng_dir = lng >= 0 ? "E" : "W"
201
170
  lat_abs = lat.abs
202
171
  lng_abs = lng.abs
203
- if FORMATS[:location] == :DMS
204
- lat_d = lat_abs.floor
205
- lat_m = ((lat_abs - lat_d) * 60).floor
206
- lat_s = ((lat_abs - lat_d - lat_m / 60.0) * 3600).round
207
- lng_d = lng_abs.floor
208
- lng_m = ((lng_abs - lng_d) * 60).floor
209
- lng_s = ((lng_abs - lng_d - lng_m / 60.0) * 3600).round
210
- "%d°%02d'%02d\"%s %d°%02d'%02d\"%s" % [ lat_d, lat_m, lat_s, lat_dir, lng_d, lng_m, lng_s, lng_dir ]
211
- else
212
- "%.4f°%s, %.4f°%s" % [ lat_abs, lat_dir, lng_abs, lng_dir ]
213
- end
172
+ lat_d = lat_abs.floor
173
+ lat_m = ((lat_abs - lat_d) * 60).floor
174
+ lat_s = ((lat_abs - lat_d - lat_m / 60.0) * 3600).round
175
+ lng_d = lng_abs.floor
176
+ lng_m = ((lng_abs - lng_d) * 60).floor
177
+ lng_s = ((lng_abs - lng_d - lng_m / 60.0) * 3600).round
178
+ "%d°%02d'%02d\"%s %d°%02d'%02d\"%s" % [lat_d, lat_m, lat_s, lat_dir, lng_d, lng_m, lng_s, lng_dir]
179
+ end
180
+ def decimal
181
+ lat_dir = lat >= 0 ? "N" : "S"
182
+ lng_dir = lng >= 0 ? "E" : "W"
183
+ lat_abs = lat.abs
184
+ lng_abs = lng.abs
185
+ "%.4f°%s, %.4f°%s" % [lat_abs, lat_dir, lng_abs, lng_dir]
214
186
  end
215
187
  def google
216
188
  # "https://www.google.com/maps/search/?api=1&query=#{lat},#{lng}&z=#{FORMATS[:zoom]}&ll=#{lat},#{lng}"
217
- "https://www.google.com/maps/place/#{lat},#{lng}/@#{lat},#{lng},#{FORMATS[:zoom]}z/"
189
+ "https://www.google.com/maps/place/#{lat},#{lng}/@#{lat},#{lng},#{IC::CONFIG.dig(:tg_bot, :link_zoom)}z/"
218
190
  end
219
191
  def yandex
220
- "https://yandex.ru/maps/?ll=#{lng},#{lat}&z=#{FORMATS[:zoom]}&pt=#{lng},#{lat},pm2rdm1"
192
+ "https://yandex.ru/maps/?ll=#{lng},#{lat}&z=#{IC::CONFIG.dig(:tg_bot, :link_zoom)}&pt=#{lng},#{lat},pm2rdm1"
221
193
  end
222
194
  def osm
223
- "https://www.openstreetmap.org/?mlat=#{lat}&mlon=#{lng}#map=#{FORMATS[:zoom]}/#{lat}/#{lng}"
195
+ "https://www.openstreetmap.org/?mlat=#{lat}&mlon=#{lng}#map=#{IC::CONFIG.dig(:tg_bot, :link_zoom)}/#{lat}/#{lng}"
224
196
  end
225
197
  def url
226
198
  osm
@@ -112,3 +112,24 @@ module INatChannel
112
112
  end
113
113
 
114
114
  end
115
+
116
+ module IC
117
+
118
+ TAXA_ICONS = INatChannel::Icons::TAXA_ICONS
119
+ ICONS = INatChannel::Icons::ICONS
120
+
121
+ def taxon_icon taxon
122
+ INatChannel::Icons::taxon_icon taxon
123
+ end
124
+
125
+ def ancestors_icon ancestor_ids
126
+ INatChannel::Icons::ancestors_icon ancestor_ids
127
+ end
128
+
129
+ def clock_icon time
130
+ INatChannel::Icons::clock_icon time
131
+ end
132
+
133
+ module_function :taxon_icon, :ancestors_icon, :clock_icon
134
+
135
+ end
@@ -12,13 +12,13 @@ module INatChannel
12
12
  class << self
13
13
 
14
14
  def acquire!
15
- file = INatChannel::CONFIG[:lock_file]
15
+ file = IC::CONFIG.dig(:lock_file, :path)
16
16
  FileUtils.mkdir_p File.dirname(file)
17
17
 
18
18
  if File.exist?(file)
19
19
  data = load_data file
20
20
  if stale?(data)
21
- INatChannel::LOGGER.info "Remove stale lock: #{file}"
21
+ IC::logger.info "Remove stale lock: #{file}"
22
22
  File.delete file
23
23
  else
24
24
  raise "Another instance is already running (PID: #{data[:pid]})"
@@ -30,15 +30,15 @@ module INatChannel
30
30
  started_at: Time.now.utc.iso8601
31
31
  }
32
32
  File.write file, JSON.pretty_generate(data)
33
- INatChannel::LOGGER.info "Lock acquired: #{file}"
33
+ IC::logger.info "Lock acquired: #{file}"
34
34
  end
35
35
 
36
36
  def release!
37
- file = INatChannel::CONFIG[:lock_file]
37
+ file = IC::CONFIG.dig(:lock_file, :path)
38
38
  return nil unless File.exist?(file)
39
39
 
40
40
  File.delete file
41
- INatChannel::LOGGER.info "Lock release: #{file}"
41
+ IC::logger.info "Lock release: #{file}"
42
42
  end
43
43
 
44
44
  private
@@ -49,7 +49,7 @@ module INatChannel
49
49
  {}
50
50
  end
51
51
 
52
- LOCK_TTL = 1800 # 30 min
52
+ LOCK_TTL = 300 # 5 min
53
53
 
54
54
  def stale? data
55
55
  if data[:started_at]
@@ -14,7 +14,7 @@ module INatChannel
14
14
 
15
15
  def get_logger
16
16
  lgr = ::Logger::new $stderr
17
- lgr.level = INatChannel::CONFIG[:log_level]
17
+ lgr.level = IC::CONFIG[:log_level]
18
18
  lgr
19
19
  end
20
20
 
@@ -22,7 +22,15 @@ module INatChannel
22
22
 
23
23
  end
24
24
 
25
- LOGGER = Logger::logger
25
+ end
26
+
27
+ module IC
28
+
29
+ def logger
30
+ INatChannel::Logger::logger
31
+ end
32
+
33
+ module_function :logger
26
34
 
27
35
  end
28
36
 
@@ -12,10 +12,10 @@ module INatChannel
12
12
  class << self
13
13
 
14
14
  def make_message observation
15
- template = if INatChannel::CONFIG[:template]
16
- INatChannel::Template::load INatChannel::CONFIG[:template]
15
+ template = if IC::CONFIG.dig(:tg_bot, :template)
16
+ IC::load_template IC::CONFIG.dig(:tg_bot, :template)
17
17
  else
18
- INatChannel::Template::default
18
+ IC::default_template
19
19
  end
20
20
  template.process observation
21
21
  end
@@ -30,3 +30,18 @@ module INatChannel
30
30
  end
31
31
 
32
32
  end
33
+
34
+ module IC
35
+
36
+ def make_message observation
37
+ INatChannel::Message::make_message observation
38
+ end
39
+
40
+ def list_photos observation
41
+ INatChannel::Message::list_photos observation
42
+ end
43
+
44
+ module_function :make_message, :list_photos
45
+
46
+ end
47
+
@@ -11,30 +11,29 @@ module INatChannel
11
11
  TELEGRAM_API = 'https://api.telegram.org/bot'
12
12
 
13
13
  def send_observation observation
14
- photos = INatChannel::Message::list_photos observation
15
- message = INatChannel::Message::make_message observation
14
+ photos = IC::list_photos observation
15
+ message = IC::make_message observation
16
16
 
17
17
  unless photos.empty?
18
- msg_id = send_media_group INatChannel::CONFIG[:chat_id], photos[0..9], message
18
+ msg_id = send_media_group IC::CONFIG.dig(:tg_bot, :chat_id), photos[0..9], message
19
19
  else
20
- msg_id = send_message INatChannel::CONFIG[:chat_id], message
20
+ msg_id = send_message IC::CONFIG.dig(:tg_bot, :chat_id), message
21
21
  end
22
22
 
23
- INatChannel::Data::sent[observation[:uuid]] = { msg_id: msg_id, sent_at: Time.now.to_s }
24
- INatChannel::LOGGER.info "Posted #{observation[:id]} (#{photos.size} photos)"
23
+ IC::logger.info "Posted #{observation[:id]} (#{photos.size} photos)"
25
24
  msg_id
26
25
  end
27
26
 
28
27
  def notify_admin text
29
- send_message(INatChannel::CONFIG[:admin_telegram_id], "iNatChannel: #{text}")
28
+ send_message(IC::CONFIG.dig(:tg_bot, :admin_id), "iNatChannel: #{text}")
30
29
  rescue
31
- INatChannel::LOGGER.error "Admin notify failed"
30
+ IC::logger.error "Admin notify failed"
32
31
  end
33
32
 
34
33
  private
35
34
 
36
35
  def token
37
- @token ||= INatChannel::CONFIG[:telegram_bot_token]
36
+ @token ||= IC::CONFIG.dig(:tg_bot, :token)
38
37
  end
39
38
 
40
39
  def send_message chat_id, text
@@ -71,11 +70,15 @@ module INatChannel
71
70
 
72
71
  def faraday
73
72
  @faraday ||= Faraday.new do |f|
74
- f.request :retry, max: INatChannel::CONFIG[:retries], interval: 2.0, interval_randomness: 0.5,
73
+ f.request :retry,
74
+ max: IC::CONFIG.dig(:tg_bot, :retries),
75
+ interval: IC::CONFIG.dig(:tg_bot, :interval),
76
+ interval_randomness: IC::CONFIG.dig(:tg_bot, :randomness),
77
+ backoff_factor: IC::CONFIG.dig(:tg_bot, :backoff),
75
78
  exceptions: [ Faraday::TimeoutError, Faraday::ConnectionFailed, Faraday::SSLError, Faraday::ClientError ]
76
79
 
77
- if INatChannel::LOGGER.level == ::Logger::DEBUG
78
- f.response :logger, INatChannel::LOGGER, bodies: true, headers: true
80
+ if IC::logger.level == ::Logger::DEBUG
81
+ f.response :logger, IC::logger, bodies: true, headers: true
79
82
  end
80
83
 
81
84
  f.adapter Faraday.default_adapter
@@ -87,3 +90,18 @@ module INatChannel
87
90
  end
88
91
 
89
92
  end
93
+
94
+ module IC
95
+
96
+ def send_observation observation
97
+ INatChannel::Telegram::send_observation observation
98
+ end
99
+
100
+ def notify_admin text
101
+ INatChannel::Telegram::notify_admin text
102
+ end
103
+
104
+ module_function :send_observation, :notify_admin
105
+
106
+ end
107
+
@@ -14,13 +14,13 @@ module INatChannel
14
14
  @template = template
15
15
  @data = data
16
16
  @renderer = ERB::new @template, trim_mode: '-'
17
- INatChannel::Icons::TAXA_ICONS.merge! data[:taxa_icons] if data[:taxa_icons]
18
- INatChannel::Icons::ICONS.merge! data[:icons] if data[:icons]
19
- INatChannel::FORMATS.merge! data[:formats] if data[:formats]
17
+ IC::TAXA_ICONS.merge! data[:taxa_icons] if data[:taxa_icons]
18
+ IC::ICONS.merge! data[:icons] if data[:icons]
19
+ IC::FORMATS.merge! data[:formats] if data[:formats]
20
20
  end
21
21
 
22
22
  def process observation_source
23
- observation = INatChannel::DataConvert::convert_observation observation_source
23
+ observation = IC::convert_observation observation_source
24
24
  vars = {
25
25
  observation: observation,
26
26
  datetime: observation.datetime,
@@ -30,8 +30,10 @@ module INatChannel
30
30
  user: observation.user,
31
31
  date: observation.date,
32
32
  time: observation.time,
33
- icons: INatChannel::Icons::ICONS,
34
- taxa_icons: INatChannel::Icons::TAXA_ICONS
33
+ icons: IC::ICONS,
34
+ taxa_icons: IC::TAXA_ICONS,
35
+ config: IC::CONFIG,
36
+ data: @data
35
37
  }
36
38
  @renderer.result_with_hash vars
37
39
  end
@@ -58,14 +60,14 @@ module INatChannel
58
60
  DEFAULT_TEMPLATE = <<~ERB
59
61
  <%= taxon.icon %> <a href="<%= taxon.url %>"><%= taxon.title %></a>
60
62
 
61
- <%= observation.icon %> <a href="<%= observation.url %>">#<%= observation.id %></a>
63
+ <%= observation.icon %> <%= observation.url %>
62
64
  <%= datetime.icon %> <%= datetime %>
63
65
  <%= user.icon %> <a href="<%= user.url %>"><%= user.title %></a>
64
66
  <% if observation.description -%>
65
67
  <blockquote><%= observation.description.text %></blockquote>
66
68
  <% end -%>
67
69
 
68
- <%= location.icon %> <%= location.title %> • <a href="<%= location.google %>">G</a> <a href="<%= location.osm %>">OSM</a>
70
+ <%= location.icon %> <%= location.dms %> • <a href="<%= location.google %>">G</a> <a href="<%= location.osm %>">OSM</a>
69
71
  <% if places && places.size > 0 -%>
70
72
  <% places.each do |place| -%>
71
73
  <%= place.icon %> <a href="<%= place.link %>"><%= place.text %></a> <%= '• #' + place.tag if place.tag %>
@@ -74,9 +76,11 @@ module INatChannel
74
76
  <%= icons[:place] %> <%= observation.place_guess %>
75
77
  <% end -%>
76
78
 
77
- <%= taxon.to_tags.join(' • ') %>
79
+ <%= taxon.to_tags&.join(' • ') %>
78
80
  ERB
79
81
 
82
+ private_constant :DEFAULT_TEMPLATE
83
+
80
84
  def default
81
85
  @default ||= new(DEFAULT_TEMPLATE, {}).freeze
82
86
  end
@@ -88,3 +92,15 @@ module INatChannel
88
92
  end
89
93
 
90
94
  end
95
+
96
+ module IC
97
+
98
+ def load_template path
99
+ INatChannel::Template::load path
100
+ end
101
+
102
+ def default_template
103
+ INatChannel::Template::default
104
+ end
105
+
106
+ end
@@ -1,6 +1,11 @@
1
-
2
1
  module INatChannel
2
+
3
+ VERSION = '0.9.2'
4
+
5
+ end
3
6
 
4
- VERSION = '0.8.2'
7
+ module IC
8
+
9
+ VERSION = INatChannel::VERSION
5
10
 
6
11
  end
data/lib/inat-channel.rb CHANGED
@@ -8,4 +8,3 @@ require_relative 'inat-channel/api'
8
8
  require_relative 'inat-channel/message'
9
9
  require_relative 'inat-channel/telegram'
10
10
 
11
- INCh = INatChannel
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inat-channel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Shikhalev
@@ -107,34 +107,6 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '3.23'
110
- - !ruby/object:Gem::Dependency
111
- name: tmpdir
112
- requirement: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - "~>"
115
- - !ruby/object:Gem::Version
116
- version: 0.2.0
117
- type: :development
118
- prerelease: false
119
- version_requirements: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - "~>"
122
- - !ruby/object:Gem::Version
123
- version: 0.2.0
124
- - !ruby/object:Gem::Dependency
125
- name: climate_control
126
- requirement: !ruby/object:Gem::Requirement
127
- requirements:
128
- - - "~>"
129
- - !ruby/object:Gem::Version
130
- version: '1.2'
131
- type: :development
132
- prerelease: false
133
- version_requirements: !ruby/object:Gem::Requirement
134
- requirements:
135
- - - "~>"
136
- - !ruby/object:Gem::Version
137
- version: '1.2'
138
110
  description: 'iNaturalist Telegram Bot: Posts random popular observations from configurable
139
111
  API queries.'
140
112
  email: