inat-channel 0.8.0.14

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.
@@ -0,0 +1,139 @@
1
+ require 'set'
2
+ require 'sanitize'
3
+ require_relative 'config'
4
+ require_relative 'icons'
5
+
6
+ module INatChannel
7
+
8
+ module Message
9
+
10
+ class << self
11
+
12
+ 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")
19
+ end
20
+
21
+ def list_photos observation
22
+ return [] unless observation[:photos]
23
+ observation[:photos].map { |ph| ph[:url].gsub("square", "large") }
24
+ end
25
+
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
+ end
136
+
137
+ end
138
+
139
+ end
@@ -0,0 +1,89 @@
1
+ require_relative 'config'
2
+ require_relative 'logger'
3
+ require_relative 'message'
4
+
5
+ module INatChannel
6
+
7
+ module Telegram
8
+
9
+ class << self
10
+
11
+ TELEGRAM_API = 'https://api.telegram.org/bot'
12
+
13
+ def send_observation observation
14
+ photos = INatChannel::Message::list_photos observation
15
+ message = INatChannel::Message::make_message observation
16
+
17
+ unless photos.empty?
18
+ msg_id = send_media_group INatChannel::CONFIG[:chat_id], photos[0..9], message
19
+ else
20
+ msg_id = send_message INatChannel::CONFIG[:chat_id], message
21
+ end
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)"
25
+ msg_id
26
+ end
27
+
28
+ def notify_admin text
29
+ send_message(INatChannel::CONFIG[:admin_telegram_id], "iNatChannel: #{text}")
30
+ rescue
31
+ INatChannel::LOGGER.error "Admin notify failed"
32
+ end
33
+
34
+ private
35
+
36
+ def token
37
+ @token ||= INatChannel::CONFIG[:telegram_bot_token]
38
+ end
39
+
40
+ def send_message chat_id, text
41
+ response = faraday.post "#{TELEGRAM_API}#{token}/sendMessage" do |req|
42
+ req.params['chat_id'] = chat_id
43
+ req.headers['Content-Type'] = 'application/json'
44
+ req.body = { text: text, parse_mode: 'HTML' }.to_json
45
+ end
46
+
47
+ data = JSON.parse response.body, symbolize_names: true
48
+ raise "Telegram error: #{data[:description]} (#{data[:error_code]})" unless data[:ok]
49
+ data[:result][:message_id]
50
+ end
51
+
52
+ def send_media_group chat_id, photo_urls, caption
53
+ media = photo_urls.map.with_index do |url, i|
54
+ if i == photo_urls.size - 1
55
+ { type: 'photo', media: url, caption: caption, parse_mode: 'HTML' }
56
+ else
57
+ { type: 'photo', media: url }
58
+ end
59
+ end.to_json
60
+
61
+ response = faraday.post "#{TELEGRAM_API}#{token}/sendMediaGroup" do |req|
62
+ req.params['chat_id'] = chat_id
63
+ req.headers['Content-Type'] = 'application/json'
64
+ req.body = { media: media }.to_json
65
+ end
66
+
67
+ data = JSON.parse response.body, symbolize_names: true
68
+ raise "Telegram error: #{data[:description]} (#{data[:error_code]})" unless data[:ok]
69
+ data[:result].last[:message_id]
70
+ end
71
+
72
+ def faraday
73
+ @faraday ||= Faraday.new do |f|
74
+ f.request :retry, max: INatChannel::CONFIG[:retries], interval: 2.0, interval_randomness: 0.5,
75
+ exceptions: [ Faraday::TimeoutError, Faraday::ConnectionFailed, Faraday::SSLError, Faraday::ClientError ]
76
+
77
+ if INatChannel::LOGGER.level == ::Logger::DEBUG
78
+ f.response :logger, INatChannel::LOGGER, bodies: true, headers: true
79
+ end
80
+
81
+ f.adapter Faraday.default_adapter
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ end
88
+
89
+ end
@@ -0,0 +1,6 @@
1
+
2
+ module INatChannel
3
+
4
+ VERSION = '0.8.0.14'
5
+
6
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'inat-channel/version'
2
+ require_relative 'inat-channel/config'
3
+ require_relative 'inat-channel/logger'
4
+ require_relative 'inat-channel/lock'
5
+ require_relative 'inat-channel/icons'
6
+ require_relative 'inat-channel/data'
7
+ require_relative 'inat-channel/api'
8
+ require_relative 'inat-channel/message'
9
+ require_relative 'inat-channel/telegram'
10
+
11
+ INCh = INatChannel
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: inat-channel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0.14
5
+ platform: ruby
6
+ authors:
7
+ - Ivan Shikhalev
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.14'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.14'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday-retry
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: sanitize
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '7.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '7.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.13'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.13'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '13.3'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '13.3'
82
+ - !ruby/object:Gem::Dependency
83
+ name: simplecov
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 0.22.0
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 0.22.0
96
+ - !ruby/object:Gem::Dependency
97
+ name: webmock
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.23'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
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
+ description: 'iNaturalist Telegram Bot: Posts random popular observations from configurable
139
+ API queries.'
140
+ email:
141
+ - shikhalev@gmail.com
142
+ executables:
143
+ - inat-channel
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - LICENSE
148
+ - README.md
149
+ - bin/inat-channel
150
+ - lib/inat-channel.rb
151
+ - lib/inat-channel/api.rb
152
+ - lib/inat-channel/config.rb
153
+ - lib/inat-channel/data.rb
154
+ - lib/inat-channel/icons.rb
155
+ - lib/inat-channel/lock.rb
156
+ - lib/inat-channel/logger.rb
157
+ - lib/inat-channel/message.rb
158
+ - lib/inat-channel/telegram.rb
159
+ - lib/inat-channel/version.rb
160
+ homepage: https://github.com/inat-get/inat-channel
161
+ licenses:
162
+ - GPL-3.0-or-later
163
+ metadata: {}
164
+ rdoc_options: []
165
+ require_paths:
166
+ - lib
167
+ required_ruby_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - "~>"
170
+ - !ruby/object:Gem::Version
171
+ version: '3.4'
172
+ required_rubygems_version: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - ">="
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
177
+ requirements: []
178
+ rubygems_version: 3.6.9
179
+ specification_version: 4
180
+ summary: iNat Telegram Poster
181
+ test_files: []