inat-channel 0.8.0.14 → 0.9.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/README.md +327 -94
- data/bin/inat-channel +14 -15
- data/lib/inat-channel/api.rb +55 -31
- data/lib/inat-channel/config.rb +60 -14
- data/lib/inat-channel/data.rb +267 -36
- data/lib/inat-channel/data_convert.rb +108 -0
- data/lib/inat-channel/data_types.rb +201 -0
- data/lib/inat-channel/icons.rb +118 -41
- data/lib/inat-channel/lock.rb +6 -6
- data/lib/inat-channel/logger.rb +10 -2
- data/lib/inat-channel/message.rb +20 -112
- data/lib/inat-channel/telegram.rb +30 -12
- data/lib/inat-channel/template.rb +106 -0
- data/lib/inat-channel/version.rb +7 -2
- data/lib/inat-channel.rb +0 -1
- metadata +4 -29
data/lib/inat-channel/message.rb
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
template = if IC::CONFIG.dig(:tg_bot, :template)
|
|
16
|
+
IC::load_template IC::CONFIG.dig(:tg_bot, :template)
|
|
17
|
+
else
|
|
18
|
+
IC::default_template
|
|
19
|
+
end
|
|
20
|
+
template.process observation
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def list_photos observation
|
|
@@ -23,117 +25,23 @@ module INatChannel
|
|
|
23
25
|
observation[:photos].map { |ph| ph[:url].gsub("square", "large") }
|
|
24
26
|
end
|
|
25
27
|
|
|
26
|
-
|
|
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
|
|
28
|
+
end
|
|
122
29
|
|
|
123
|
-
|
|
124
|
-
"\##{name.gsub(".", "").gsub("-", "").gsub(" ", "_")}"
|
|
125
|
-
end
|
|
30
|
+
end
|
|
126
31
|
|
|
127
|
-
|
|
128
|
-
return nil unless observation[:geojson]&.[](:coordinates) && observation[:geojson][:type] == "Point"
|
|
32
|
+
end
|
|
129
33
|
|
|
130
|
-
|
|
131
|
-
url = "https://maps.google.com/?q=#{lat},#{lon}"
|
|
132
|
-
"<a href='#{url}'>#{lat.round(4)}°N, #{lon.round(4)}°E</a>"
|
|
133
|
-
end
|
|
34
|
+
module IC
|
|
134
35
|
|
|
135
|
-
|
|
36
|
+
def make_message observation
|
|
37
|
+
INatChannel::Message::make_message observation
|
|
38
|
+
end
|
|
136
39
|
|
|
40
|
+
def list_photos observation
|
|
41
|
+
INatChannel::Message::list_photos observation
|
|
137
42
|
end
|
|
138
43
|
|
|
44
|
+
module_function :make_message, :list_photos
|
|
45
|
+
|
|
139
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 =
|
|
15
|
-
message =
|
|
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
|
|
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
|
|
20
|
+
msg_id = send_message IC::CONFIG.dig(:tg_bot, :chat_id), message
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
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(
|
|
28
|
+
send_message(IC::CONFIG.dig(:tg_bot, :admin_id), "iNatChannel: #{text}")
|
|
30
29
|
rescue
|
|
31
|
-
|
|
30
|
+
IC::logger.error "Admin notify failed"
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
private
|
|
35
34
|
|
|
36
35
|
def token
|
|
37
|
-
@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,
|
|
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
|
|
78
|
-
f.response :logger,
|
|
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
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
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
|
+
end
|
|
21
|
+
|
|
22
|
+
def process observation_source
|
|
23
|
+
observation = IC::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: IC::ICONS,
|
|
34
|
+
taxa_icons: IC::TAXA_ICONS,
|
|
35
|
+
config: IC::CONFIG,
|
|
36
|
+
data: @data
|
|
37
|
+
}
|
|
38
|
+
@renderer.result_with_hash vars
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
|
|
43
|
+
def load path
|
|
44
|
+
content = File.read path
|
|
45
|
+
if content.lines(chomp: true).first == '---'
|
|
46
|
+
docs = content.split(/^---\n/m, 3)
|
|
47
|
+
data = if docs[0].strip.empty? && docs[1]
|
|
48
|
+
YAML.safe_load docs[1], symbolize_names: true
|
|
49
|
+
else
|
|
50
|
+
YAML.safe_load docs[0], symbolize_names: true
|
|
51
|
+
end || {}
|
|
52
|
+
template = docs[2] || docs[1] || content
|
|
53
|
+
else
|
|
54
|
+
data = {}
|
|
55
|
+
template = content
|
|
56
|
+
end
|
|
57
|
+
new(template, data)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
DEFAULT_TEMPLATE = <<~ERB
|
|
61
|
+
<%= taxon.icon %> <a href="<%= taxon.url %>"><%= taxon.title %></a>
|
|
62
|
+
|
|
63
|
+
<%= observation.icon %> <%= observation.url %>
|
|
64
|
+
<%= datetime.icon %> <%= datetime %>
|
|
65
|
+
<%= user.icon %> <a href="<%= user.url %>"><%= user.title %></a>
|
|
66
|
+
<% if observation.description -%>
|
|
67
|
+
<blockquote><%= observation.description.text %></blockquote>
|
|
68
|
+
<% end -%>
|
|
69
|
+
|
|
70
|
+
<%= location.icon %> <%= location.dms %> • <a href="<%= location.google %>">G</a> <a href="<%= location.osm %>">OSM</a>
|
|
71
|
+
<% if places && places.size > 0 -%>
|
|
72
|
+
<% places.each do |place| -%>
|
|
73
|
+
<%= place.icon %> <a href="<%= place.link %>"><%= place.text %></a> <%= '• #' + place.tag if place.tag %>
|
|
74
|
+
<% end -%>
|
|
75
|
+
<% else -%>
|
|
76
|
+
<%= icons[:place] %> <%= observation.place_guess %>
|
|
77
|
+
<% end -%>
|
|
78
|
+
|
|
79
|
+
<%= taxon.to_tags&.join(' • ') %>
|
|
80
|
+
ERB
|
|
81
|
+
|
|
82
|
+
private_constant :DEFAULT_TEMPLATE
|
|
83
|
+
|
|
84
|
+
def default
|
|
85
|
+
@default ||= new(DEFAULT_TEMPLATE, {}).freeze
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private :new
|
|
89
|
+
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
end
|
|
93
|
+
|
|
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
|
data/lib/inat-channel/version.rb
CHANGED
data/lib/inat-channel.rb
CHANGED
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.
|
|
4
|
+
version: 0.9.0
|
|
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:
|
|
@@ -151,11 +123,14 @@ files:
|
|
|
151
123
|
- lib/inat-channel/api.rb
|
|
152
124
|
- lib/inat-channel/config.rb
|
|
153
125
|
- lib/inat-channel/data.rb
|
|
126
|
+
- lib/inat-channel/data_convert.rb
|
|
127
|
+
- lib/inat-channel/data_types.rb
|
|
154
128
|
- lib/inat-channel/icons.rb
|
|
155
129
|
- lib/inat-channel/lock.rb
|
|
156
130
|
- lib/inat-channel/logger.rb
|
|
157
131
|
- lib/inat-channel/message.rb
|
|
158
132
|
- lib/inat-channel/telegram.rb
|
|
133
|
+
- lib/inat-channel/template.rb
|
|
159
134
|
- lib/inat-channel/version.rb
|
|
160
135
|
homepage: https://github.com/inat-get/inat-channel
|
|
161
136
|
licenses:
|