inat-channel 0.8.0.14 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f68cf20d6a12a86d06bf3fdfbe2f8e61d6890060275e713e3e1ef530404a05d8
4
- data.tar.gz: feae77da45b8ae5cc418407e06b3179891764fc9def665b48f8101499029335d
3
+ metadata.gz: 0c91fbb67ba53ef0148d0c9074551a3c8591cb05b6d3a578858018459213f8c4
4
+ data.tar.gz: b7cefeea01cf40c26574cee11f8ac62657a57435732ed095b3badeebf5c79709
5
5
  SHA512:
6
- metadata.gz: 4316d7ba2c98e45666a52b747c3eed6be62a09ca3f18253f09060cdc355f2f4c9c73b16add00481459262cec06290ea984ad6c14a27f2bd238f9d386f565f909
7
- data.tar.gz: fdd98d39bb81f9999968a6ab2577ef208eccf9ff847988d1eff07051d39108e3c86b14e17dad40ae58573fcc11b3ba16cb4e357712a9fe01f27f186bf3d1e21c
6
+ metadata.gz: dc896ae4f4a74f64364d1607aabdf2c6acc3d2a3e86e0d5d82e498f5da491201fba99d4c5f1ab769694365914874d401faa85dae97d951695895adfa074731b8
7
+ data.tar.gz: 67f3314b776a1352a0451570e9d6589abbe9467cfa44b1a124cf4f4d00ed2c7fda8db6060b2e7d2d0021dfde43858abc6484d87569b33b232300932e6f88dc8d
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # iNat Telegram Poster
2
2
 
3
3
  [![GitHub License](https://img.shields.io/github/license/inat-get/inat-channel)](LICENSE)
4
- [![Gem Version](https://badge.fury.io/rb/inat-channel.svg?icon=si%3Arubygems&d=3)](https://badge.fury.io/rb/inat-channel)
4
+ [![Gem Version](https://badge.fury.io/rb/inat-channel.svg?icon=si%3Arubygems&d=5)](https://badge.fury.io/rb/inat-channel)
5
5
  [![Ruby](https://github.com/inat-get/inat-channel/actions/workflows/ruby.yml/badge.svg)](https://github.com/inat-get/inat-channel/actions/workflows/ruby.yml)
6
6
  ![Coverage](coverage-badge.svg)
7
7
 
@@ -17,7 +17,8 @@ module INatChannel
17
17
  API_ENDPOINT = 'https://api.inaturalist.org/v2/observations'
18
18
  LIST_FIELDS = 'uuid'
19
19
  SINGLE_FIELDS = '(id:!t,uuid:!t,uri:!t,geojson:(all:!t),user:(login:!t,name:!t),taxon:(ancestor_ids:!t,preferred_common_name:!t,name:!t),' +
20
- 'place_ids:!t,place_guess:!t,observed_on_string:!t,description:!t,photos:(url:!t),identifications:(taxon:(ancestors:(name:!t))))'
20
+ 'place_ids:!t,place_guess:!t,observed_on_string:!t,description:!t,photos:(url:!t),time_observed_at:!t,' +
21
+ 'identifications:(taxon:(ancestors:(name:!t))))'
21
22
 
22
23
  private_constant :PER_PAGE, :PAGE_DELAY, :API_ENDPOINT, :LIST_FIELDS, :SINGLE_FIELDS
23
24
 
@@ -56,7 +56,14 @@ module INatChannel
56
56
 
57
57
  def load_config path
58
58
  raise "Config file not found: #{path}" unless File.exist?(path)
59
- YAML.safe_load_file(path, symbolize_names: true)
59
+ cfg = YAML.safe_load_file path, symbolize_names: true
60
+ if String === cfg[:places]
61
+ path = File.expand_path(path)
62
+ places_path = File.expand_path(cfg[:places], File.dirname(path))
63
+ places = YAML.safe_load_file places_path, symbolize_names: true
64
+ cfg[:places] = places
65
+ end
66
+ cfg
60
67
  end
61
68
 
62
69
  def load_env
@@ -0,0 +1,92 @@
1
+ require 'date'
2
+ require 'time'
3
+ require 'set'
4
+
5
+ require_relative 'data_types'
6
+
7
+ module INatChannel
8
+
9
+ module DataConvert
10
+
11
+ class << self
12
+
13
+ def convert_observation observation_source
14
+ id = observation_source[:id]
15
+ url = observation_source[:uri]
16
+ uuid = observation_source[:uuid]
17
+ user = convert_user observation_source[:user]
18
+ taxon = convert_taxon observation_source[:taxon], observation_source[:identifications]
19
+ places = convert_places observation_source[:place_ids]
20
+ datetime = DateTime.parse observation_source[:time_observed_at]
21
+ location = convert_location observation_source[:geojson]
22
+ description = convert_description observation_source[:description]
23
+ place_guess = observation_source[:place_guess]
24
+ Observation::new id: id, url: url, uuid: uuid, user: user, taxon: taxon, places: places, datetime: datetime, location: location, description: description, place_guess: place_guess
25
+ end
26
+
27
+ private
28
+
29
+ def convert_location location_source
30
+ return nil unless location_source
31
+ lat = location_source.dig :coordinates, 1
32
+ lng = location_source.dig :coordinates, 0
33
+ return nil unless lat && lng
34
+ Location::new lat: lat, lng: lng
35
+ end
36
+
37
+ def convert_description description_source
38
+ return nil if description_source.nil? || description_source.empty?
39
+ Description::new value: description_source
40
+ end
41
+
42
+ def convert_user user_source
43
+ User::new id: user_source[:id], login: user_source[:login], name: user_source[:name]
44
+ end
45
+
46
+ def convert_taxon taxon_source, identifications
47
+ id = taxon_source[:id]
48
+ scientific_name = taxon_source[:name]
49
+ common_name = taxon_source[:preferred_common_name]
50
+ source_ancestors = nil
51
+ identifications.each do |ident|
52
+ it = ident[:taxon]
53
+ if it[:id] == id
54
+ source_ancestors = it[:ancestors]
55
+ break
56
+ end
57
+ end
58
+ ancestors = if source_ancestors
59
+ source_ancestors.map do |anc|
60
+ Ancestor::new id: anc[:id], scientific_name: anc[:name]
61
+ end
62
+ else
63
+ taxon_source[:ancestor_ids].map do |aid|
64
+ Ancestor::new id: aid
65
+ end
66
+ # TODO: load names from API by ancestor_ids
67
+ end
68
+ ancestors << Ancestor::new(id: taxon_source[:id], scientific_name: taxon_source[:name])
69
+ Taxon::new id: id, scientific_name: scientific_name, common_name: common_name, ancestors: ancestors
70
+ end
71
+
72
+ def convert_places place_ids
73
+ places_config = INatChannel::CONFIG[:places]
74
+ return nil unless places_config
75
+ result = []
76
+ places_config.each do |_, items|
77
+ items.each do |item|
78
+ ids = Set[*item[:place_ids]]
79
+ if ids.intersect?(place_ids)
80
+ result << Place::new(text: item[:text], link: item[:link], tag: item[:tag])
81
+ break
82
+ end
83
+ end
84
+ end
85
+ result
86
+ end
87
+
88
+ end
89
+
90
+ end
91
+
92
+ end
@@ -0,0 +1,229 @@
1
+ require 'date'
2
+ require 'time'
3
+ require 'sanitize'
4
+
5
+ require_relative 'icons'
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
+ class Date
19
+ 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
29
+ end
30
+ end
31
+
32
+ class Time
33
+ 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
43
+ end
44
+ end
45
+
46
+ class DateTime
47
+ 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
57
+ end
58
+ end
59
+
60
+ class String
61
+ def to_tag
62
+ "\##{self.gsub(/\s+/, '_').gsub(/-/, '_').gsub(/[^a-zA-Zа-яА-ЯёЁ_]/, '')}"
63
+ end
64
+ def limit len
65
+ return self if length <= len
66
+
67
+ short = self[0, len]
68
+ last_space = short.rindex(/\s/)
69
+ last_sign = short.rindex(/[,.;:!?]/)
70
+ if last_space
71
+ if last_sign && last_sign + 1 > last_space
72
+ return short[0, last_sign + 1] + '...'
73
+ end
74
+ return short[0, last_space] + '...'
75
+ else
76
+ if last_sign
77
+ return short[0, last_sign + 1] + '...'
78
+ end
79
+ return short + '...'
80
+ end
81
+ end
82
+ end
83
+
84
+ class Array
85
+ def to_tags
86
+ self.map do |item|
87
+ if item.respond_to?(:to_tag)
88
+ item.to_tag
89
+ else
90
+ item&.to_s.to_tag
91
+ end
92
+ end.compact
93
+ end
94
+ end
95
+
96
+ Observation = Struct::new :taxon, :id, :uuid, :url, :user, :datetime, :places, :place_guess, :description, :location, keyword_init: true do
97
+ def icon
98
+ INatChannel::Icons::ICONS[:observation]
99
+ end
100
+ def date
101
+ datetime.to_date
102
+ end
103
+ def time
104
+ datetime.to_time
105
+ end
106
+ end
107
+
108
+ Taxon = Struct::new :scientific_name, :common_name, :id, :ancestors, keyword_init: true do
109
+ def icon
110
+ INatChannel::Icons::ancestors_icon ancestors.map(&:id)
111
+ end
112
+ def title
113
+ if common_name && !common_name.empty?
114
+ "<b>#{common_name}</b> <i>(#{scientific_name})</i>"
115
+ else
116
+ "<b><i>#{scientific_name}</i></b>"
117
+ end
118
+ end
119
+ def url
120
+ "https://www.inaturalist.org/taxa/#{id}"
121
+ end
122
+ def to_tags
123
+ ancestors.to_tags
124
+ end
125
+ end
126
+
127
+ Ancestor = Struct::new :scientific_name, :id, keyword_init: true do
128
+ def to_tag
129
+ scientific_name.to_tag
130
+ end
131
+ end
132
+
133
+ Place = Struct::new :ids, :text, :link, :tag, keyword_init: true do
134
+ def icon
135
+ INatChannel::Icons::ICONS[:place]
136
+ end
137
+ def to_tag
138
+ tag&.to_tag
139
+ end
140
+ def title
141
+ text
142
+ end
143
+ def url
144
+ link
145
+ end
146
+ end
147
+
148
+ User = Struct::new :id, :login, :name, keyword_init: true do
149
+ def icon
150
+ INatChannel::Icons::ICONS[:user]
151
+ end
152
+ def title
153
+ if name && !name.empty?
154
+ name
155
+ else
156
+ "@#{login}"
157
+ end
158
+ end
159
+ def url
160
+ "https://www.inaturalist.org/people/#{id}"
161
+ end
162
+ end
163
+
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
+ }
170
+
171
+ SANITIZE_TEXT_CONFIG = {
172
+ elements: [],
173
+ remove_contents: [ 'script', 'style' ]
174
+ }
175
+
176
+ Description = Struct::new :value, keyword_init: true do
177
+ def icon
178
+ INatChannel::Icons::ICONS[:description]
179
+ end
180
+ def text
181
+ Sanitize.fragment(value, SANITIZE_TEXT_CONFIG).limit(FORMATS[:description_limit])
182
+ end
183
+ def html
184
+ sanitized = Sanitize.fragment value, SANITIZE_HTML_CONFIG
185
+ if sanitized.length > FORMATS[:description_limit]
186
+ # В отличие от простого текста, обрезка HTML требует куда более изощренной логики, что неоправданно
187
+ text
188
+ else
189
+ sanitized
190
+ end
191
+ end
192
+ end
193
+
194
+ Location = Struct::new :lat, :lng, keyword_init: true do
195
+ def icon
196
+ INatChannel::Icons::ICONS[:location]
197
+ end
198
+ def title
199
+ lat_dir = lat >= 0 ? 'N' : 'S'
200
+ lng_dir = lng >= 0 ? 'E' : 'W'
201
+ lat_abs = lat.abs
202
+ 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
214
+ end
215
+ def google
216
+ # "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/"
218
+ end
219
+ def yandex
220
+ "https://yandex.ru/maps/?ll=#{lng},#{lat}&z=#{FORMATS[:zoom]}&pt=#{lng},#{lat},pm2rdm1"
221
+ end
222
+ def osm
223
+ "https://www.openstreetmap.org/?mlat=#{lat}&mlon=#{lng}#map=#{FORMATS[:zoom]}/#{lat}/#{lng}"
224
+ end
225
+ def url
226
+ osm
227
+ end
228
+ end
229
+
@@ -1,56 +1,112 @@
1
-
2
1
  module INatChannel
3
-
4
2
  module Icons
5
-
6
3
  TAXA_ICONS = {
7
- 48460 => '🧬',
8
- 47126 => '🌿',
9
- 47170 => '🍄',
10
- 47686 => '🦠',
11
- 151817 => '🦠',
12
- 67333 => '🦠',
13
- 1 => '🐾',
14
- 136329 => '🌲',
15
- 47124 => '🌸',
16
- 47163 => '🍃',
17
- 47178 => '🐟',
18
- 196614 => '🦈',
19
- 47187 => '🦀',
20
- 47158 => '🪲',
21
- 47119 => '🕷️',
22
- 71261 => '🦅',
23
- 18874 => '🦜',
24
- 48222 => '🌊',
25
- 47115 => '🐚',
26
- 3 => '🐦',
27
- 40151 => '🦌',
28
- 26036 => '🐍',
29
- 20978 => '🐸'
30
-
31
- # TODO: add ALL taxa with iNat icons and some other large group
32
- }.freeze
4
+ 48460 => "🧬",
5
+ 47126 => "🌿",
6
+ 47170 => "🍄",
7
+ 47686 => "🦠",
8
+ 151817 => "🦠",
9
+ 67333 => "🦠",
10
+ 1 => "🐾",
11
+ 136329 => "🌲",
12
+ 47124 => "🌸",
13
+ 47163 => "🍃",
14
+ 47178 => "🐟",
15
+ 196614 => "🦈",
16
+ 47187 => "🦀",
17
+ 47158 => "🪲",
18
+ 47119 => "🕷️",
19
+ 71261 => "🦅",
20
+ 18874 => "🦜",
21
+ 48222 => "🌊",
22
+ 47115 => "🐚",
23
+ 3 => "🐦",
24
+ 40151 => "🦌",
25
+ 26036 => "🐍",
26
+ 20978 => "🐸",
27
+
28
+ # TODO: add ALL taxa with iNat icons and some other large group
29
+ }
33
30
 
34
31
  ICONS = {
35
- :user => '👤',
36
- :place => '🗺️',
37
- :calendar => '📅',
38
- :location => '📍',
39
- :observation => '📷',
40
- :description => '📝',
41
- :default_taxon => '🧬'
42
- # TODO: add other icons like calendar, place, etc.
43
- }.freeze
32
+ :user => "👤",
33
+ :place => "🗺️",
34
+ :calendar => "📅",
35
+ :location => "📍",
36
+ :observation => "📷",
37
+ :description => "📝",
38
+ :default_taxon => "🧬",
39
+ # TODO: add other icons like calendar, place, etc.
40
+ }
44
41
 
45
42
  class << self
46
-
47
43
  def taxon_icon taxon
48
- taxon[:ancestor_ids].reverse_each do |ancestor_id|
49
- return TAXA_ICONS[ancestor_id] if TAXA_ICONS[ancestor_id]
44
+ ancestors_icon taxon[:ancestor_ids]
45
+ end
46
+
47
+ def ancestors_icon ancestor_ids
48
+ ancestor_ids.reverse_each do |ancestor_id|
49
+ return TAXA_ICONS[ancestor_id] if TAXA_ICONS[ancestor_id]
50
50
  end
51
51
  return ICONS[:default_taxon]
52
52
  end
53
53
 
54
+ def clock_icon time
55
+ hour = time.hour % 12
56
+ minute = time.min
57
+
58
+ if minute <= 20
59
+ # ≤20 мин - текущий час
60
+ case hour
61
+ when 0, 12 then "🕛"
62
+ when 1 then "🕐"
63
+ when 2 then "🕑"
64
+ when 3 then "🕒"
65
+ when 4 then "🕓"
66
+ when 5 then "🕔"
67
+ when 6 then "🕕"
68
+ when 7 then "🕖"
69
+ when 8 then "🕗"
70
+ when 9 then "🕘"
71
+ when 10 then "🕙"
72
+ when 11 then "🕚"
73
+ end
74
+ elsif minute < 40
75
+ # 21-39 мин - полчаса текущего часа
76
+ case hour
77
+ when 0, 12 then "🕧"
78
+ when 1 then "🕜"
79
+ when 2 then "🕝"
80
+ when 3 then "🕞"
81
+ when 4 then "🕟"
82
+ when 5 then "🕠"
83
+ when 6 then "🕡"
84
+ when 7 then "🕢"
85
+ when 8 then "🕣"
86
+ when 9 then "🕤"
87
+ when 10 then "🕥"
88
+ when 11 then "🕦"
89
+ end
90
+ else
91
+ # ≥40 мин - следующий час
92
+ next_hour = (hour + 1) % 12
93
+ case next_hour
94
+ when 0, 12 then "🕛"
95
+ when 1 then "🕐"
96
+ when 2 then "🕑"
97
+ when 3 then "🕒"
98
+ when 4 then "🕓"
99
+ when 5 then "🕔"
100
+ when 6 then "🕕"
101
+ when 7 then "🕖"
102
+ when 8 then "🕗"
103
+ when 9 then "🕘"
104
+ when 10 then "🕙"
105
+ when 11 then "🕚"
106
+ end
107
+ end
108
+ end
109
+
54
110
  end
55
111
 
56
112
  end
@@ -1,7 +1,9 @@
1
1
  require 'set'
2
2
  require 'sanitize'
3
+
3
4
  require_relative 'config'
4
5
  require_relative 'icons'
6
+ require_relative 'template'
5
7
 
6
8
  module INatChannel
7
9
 
@@ -10,12 +12,12 @@ module INatChannel
10
12
  class << self
11
13
 
12
14
  def make_message observation
13
- [
14
- taxon_title(observation[:taxon]),
15
- observation_block(observation),
16
- "#{INatChannel::Icons::ICONS[:location]} #{geo_link(observation)}\n" + (place_block(observation[:place_ids]) || observation[:place_guess]),
17
- ancestors_block(observation)
18
- ].join("\n\n")
15
+ template = if INatChannel::CONFIG[:template]
16
+ INatChannel::Template::load INatChannel::CONFIG[:template]
17
+ else
18
+ INatChannel::Template::default
19
+ end
20
+ template.process observation
19
21
  end
20
22
 
21
23
  def list_photos observation
@@ -23,115 +25,6 @@ module INatChannel
23
25
  observation[:photos].map { |ph| ph[:url].gsub("square", "large") }
24
26
  end
25
27
 
26
- private
27
-
28
- def taxon_title taxon
29
- icon = INatChannel::Icons::taxon_icon taxon
30
- link = "https://www.inaturalist.org/taxa/#{taxon[:id]}"
31
-
32
- common_name = taxon[:preferred_common_name]
33
- scientific_name = taxon[:name]
34
-
35
- title = if common_name
36
- "<b>#{common_name}</b> <i>(#{scientific_name})</i>"
37
- else
38
- "<b><i>#{scientific_name}</i></b>"
39
- end
40
-
41
- "#{icon} <a href='#{link}'>#{title}</a>"
42
- end
43
-
44
- def observation_block observation
45
- user = observation[:user]
46
- user_title = if user[:name] && !user[:name].empty?
47
- user[:name]
48
- else
49
- "<code>#{user[:login]}</code>"
50
- end
51
- user_link = "https://www.inaturalist.org/people/#{user[:login]}"
52
- observation_part = "#{INatChannel::Icons::ICONS[:observation]} <a href='#{observation[:uri]}'><b>\##{observation[:id]}</b></a>"
53
- user_part = "#{INatChannel::Icons::ICONS[:user]} <a href='#{user_link}'>#{user_title}</a>"
54
- date_part = "#{INatChannel::Icons::ICONS[:calendar]} #{observation[:observed_on_string]}"
55
- description = observation[:description] && Sanitize.fragment(observation[:description])
56
- description_part = if description && !description.empty?
57
- "\n#{INatChannel::Icons::ICONS[:description]} #{limit_text(description, 500)}"
58
- else
59
- ""
60
- end
61
- "#{observation_part}\n#{date_part}\n#{user_part}#{description_part}"
62
- end
63
-
64
- def limit_text text, limit
65
- return text if text.length <= limit
66
- truncated = text[0, limit]
67
- last_space = truncated.rindex(/\s/)
68
- last_sign = truncated.rindex(/[,.;:!?]/)
69
- if last_space
70
- if last_sign && last_sign + 1 > last_space
71
- return truncated[0, last_sign + 1] + '...'
72
- end
73
- return truncated[0, last_space] + '...'
74
- else
75
- if last_sign
76
- return truncated[0, last_sign + 1] + '...'
77
- end
78
- return truncated + '...'
79
- end
80
- end
81
-
82
- def place_block place_ids
83
- return nil unless CONFIG[:places]
84
-
85
- place_ids = Set[*place_ids]
86
- found = []
87
- CONFIG[:places].each do |_, list|
88
- list.each do |item|
89
- item_ids = Set[*item[:place_ids]]
90
- if place_ids.intersect?(item_ids)
91
- found << item
92
- break
93
- end
94
- end
95
- end
96
-
97
- if found.empty?
98
- nil
99
- else
100
- found.map { |i| "#{INatChannel::Icons::ICONS[:place]} <a href='#{i[:link]}'>#{i[:text]}</a>" }.join("\n")
101
- end
102
- end
103
-
104
- def ancestors_block observation
105
- taxon_id = observation[:taxon][:id]
106
- ancestors = nil
107
- observation[:identifications].each do |ident|
108
- it = ident[:taxon]
109
- if it[:id] == taxon_id
110
- ancestors = it[:ancestors]
111
- break
112
- end
113
- end
114
-
115
- if ancestors
116
- (ancestors.map { |a| name_to_hashtag(a[:name]) } + [name_to_hashtag(observation[:taxon][:name])]).join(" • ")
117
- else
118
- # TODO: load ancestors with new query...
119
- nil
120
- end
121
- end
122
-
123
- def name_to_hashtag name
124
- "\##{name.gsub(".", "").gsub("-", "").gsub(" ", "_")}"
125
- end
126
-
127
- def geo_link observation
128
- return nil unless observation[:geojson]&.[](:coordinates) && observation[:geojson][:type] == "Point"
129
-
130
- lon, lat = observation[:geojson][:coordinates]
131
- url = "https://maps.google.com/?q=#{lat},#{lon}"
132
- "<a href='#{url}'>#{lat.round(4)}°N, #{lon.round(4)}°E</a>"
133
- end
134
-
135
28
  end
136
29
 
137
30
  end
@@ -0,0 +1,90 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+
4
+ require_relative 'data_types'
5
+ require_relative 'data_convert'
6
+
7
+ module INatChannel
8
+
9
+ class Template
10
+
11
+ attr_reader :template, :data
12
+
13
+ def initialize template, data
14
+ @template = template
15
+ @data = data
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]
20
+ end
21
+
22
+ def process observation_source
23
+ observation = INatChannel::DataConvert::convert_observation observation_source
24
+ vars = {
25
+ observation: observation,
26
+ datetime: observation.datetime,
27
+ location: observation.location,
28
+ places: observation.places,
29
+ taxon: observation.taxon,
30
+ user: observation.user,
31
+ date: observation.date,
32
+ time: observation.time,
33
+ icons: INatChannel::Icons::ICONS,
34
+ taxa_icons: INatChannel::Icons::TAXA_ICONS
35
+ }
36
+ @renderer.result_with_hash vars
37
+ end
38
+
39
+ class << self
40
+
41
+ def load path
42
+ content = File.read path
43
+ if content.lines(chomp: true).first == '---'
44
+ docs = content.split(/^---\n/m, 3)
45
+ data = if docs[0].strip.empty? && docs[1]
46
+ YAML.safe_load docs[1], symbolize_names: true
47
+ else
48
+ YAML.safe_load docs[0], symbolize_names: true
49
+ end || {}
50
+ template = docs[2] || docs[1] || content
51
+ else
52
+ data = {}
53
+ template = content
54
+ end
55
+ new(template, data)
56
+ end
57
+
58
+ DEFAULT_TEMPLATE = <<~ERB
59
+ <%= taxon.icon %> <a href="<%= taxon.url %>"><%= taxon.title %></a>
60
+
61
+ <%= observation.icon %> <a href="<%= observation.url %>">#<%= observation.id %></a>
62
+ <%= datetime.icon %> <%= datetime %>
63
+ <%= user.icon %> <a href="<%= user.url %>"><%= user.title %></a>
64
+ <% if observation.description -%>
65
+ <blockquote><%= observation.description.text %></blockquote>
66
+ <% end -%>
67
+
68
+ <%= location.icon %> <%= location.title %> • <a href="<%= location.google %>">G</a> <a href="<%= location.osm %>">OSM</a>
69
+ <% if places && places.size > 0 -%>
70
+ <% places.each do |place| -%>
71
+ <%= place.icon %> <a href="<%= place.link %>"><%= place.text %></a> <%= '• #' + place.tag if place.tag %>
72
+ <% end -%>
73
+ <% else -%>
74
+ <%= icons[:place] %> <%= observation.place_guess %>
75
+ <% end -%>
76
+
77
+ <%= taxon.to_tags.join(' • ') %>
78
+ ERB
79
+
80
+ def default
81
+ @default ||= new(DEFAULT_TEMPLATE, {}).freeze
82
+ end
83
+
84
+ private :new
85
+
86
+ end
87
+
88
+ end
89
+
90
+ end
@@ -1,6 +1,6 @@
1
1
 
2
2
  module INatChannel
3
3
 
4
- VERSION = '0.8.0.14'
4
+ VERSION = '0.8.2'
5
5
 
6
6
  end
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.0.14
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Shikhalev
@@ -151,11 +151,14 @@ files:
151
151
  - lib/inat-channel/api.rb
152
152
  - lib/inat-channel/config.rb
153
153
  - lib/inat-channel/data.rb
154
+ - lib/inat-channel/data_convert.rb
155
+ - lib/inat-channel/data_types.rb
154
156
  - lib/inat-channel/icons.rb
155
157
  - lib/inat-channel/lock.rb
156
158
  - lib/inat-channel/logger.rb
157
159
  - lib/inat-channel/message.rb
158
160
  - lib/inat-channel/telegram.rb
161
+ - lib/inat-channel/template.rb
159
162
  - lib/inat-channel/version.rb
160
163
  homepage: https://github.com/inat-get/inat-channel
161
164
  licenses: