14f1152cb92ca13970971b4b212a4c21ff71b623
[iankelling.org] / _site / comment.rb
1 #!/usr/bin/env ruby
2 # encoding: utf-8
3 # Copyright (C) 2016 Ian Kelling
4
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 2 of the License, or
8 # (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18 # debian sets LANG=C when starting apache2.
19 # the envoding comment above fixes the internal encoding afaik,
20 # Found this at
21 # https://stackoverflow.com/questions/20521371/set-utf-8-as-default-for-ruby-1-9-3
22 # also note man ruby's -E arg.
23 Encoding.default_external = Encoding::UTF_8
24
25 require 'cgi'
26 require 'fileutils'
27 require 'time'
28 require 'sqlite3'
29
30 require_relative '../b'
31 include B
32
33 # constanty things
34 DEBUG = true
35 CAPTCHA = -> {
36 c = []
37 x = (<<EOF).split("\n")
38 What does a dog wag?
39 tail
40 Would you sleep better on a bed or a keyboard?
41 bed
42 Are there more or less than one million people on the Earth?
43 more
44 Which of Lilly and Robert is more commonly a woman’s name?
45 lilly
46 Which word has fewer letters, adorable or fox?
47 fox
48 What is the normal color of milk?
49 white
50 Which of Iceland and Turkey is an island nation?
51 iceland
52 Is a filesystem like a tree or a rose?
53 tree
54 What character is a tilde?
55 ~
56 Which is brighter, the moon or the sun?
57 (the )?sun
58 Which is closer, the moon or the sun?
59 (the )?moon
60 What language is this sentence written in?
61 english
62 What animal says "meow" and catches mice?
63 cat
64 What animal quacks and has webbed feet?
65 duck
66 Which are better fliers: worms or birds?
67 birds
68 Which typically runs first: a kernel or a web browser?
69 kernel
70 EOF
71 while x.length > 0
72 c << x.pop(2)
73 end
74 c
75 }[]
76
77 def do_captcha
78 captcha_q = CAPTCHA.sample[0]
79 puts "Content-type: text/html\n\n"
80 puts skel('comment.rb', "#{DN}/captcha", <<EOF, ' / <a href="/blog.html">blog</a> / comment-captcha')
81 <p>Hello friend. I haven't read a post from #{IP}, and I only remember for a few months, so:</p>
82 <p>#{captcha_q}</p>
83
84 <form action="/comment.rb" method="post">
85 <input class="misc" type="text" name="url">
86 <input name="goto" type="hidden" value="#{GOTO}">
87 <input name="question" type="hidden" value="#{captcha_q}">
88 <input name="answer">
89 <br>Your comment:
90 <textarea rows="10" name="comment" maxlength="1000">#{COMMENT_TXT}</textarea>
91 <input type="submit" value="Submit">
92 </form>
93 EOF
94 exit 0
95 end
96
97
98 def fail(msg)
99 if DEBUG and msg
100 puts "Content-type: text/plain\n\n"
101 puts msg
102 else
103 redir
104 end
105 exit 0
106 end
107
108 def redir
109 File.write('/tmp/x', GOTO)
110 puts 'Status: 302 Found'
111 puts "Location: #{GOTO}#comment-section\n\n"
112 exit(0)
113 end
114
115
116 def bn(*args)
117 File.basename *args
118 end
119
120
121 ###### begin error checking & arg parsing ######
122 cgi = CGI.new
123 IP = cgi.remote_addr
124
125 if cgi.has_key?('goto')
126 GOTO = cgi['goto']
127 else
128 GOTO = '/'
129 fail['redir to /']
130 end
131
132 if (cgi.has_key?('url') && cgi['url'] != "") || ! cgi.has_key?('comment')
133 fail["comment not in form or url in form. cgi.params: #{cgi.params}"]
134 end
135
136 COMMENT_TXT = cgi["comment"]
137
138
139 if COMMENT_TXT.length > 1000 or GOTO.length > 150
140 fail['length of comment or goto is too great']
141 end
142
143
144 captchad = false
145 if cgi.has_key?('answer') && cgi.has_key?('question')
146 if cgi['answer'].downcase !~ /^#{CAPTCHA.to_h[cgi['question']]}$/
147 do_captcha
148 end
149 captchad = true
150 end
151
152
153 -> {
154 found = false
155 Dir.foreach('blog') do |entry|
156 next if ['.','..'].any? { |f| f == entry }
157 if GOTO == 'blog/' + entry
158 found = true
159 break
160 end
161 end
162 fail['goto entry not found'] unless found
163 }[]
164 ######### end error checking & arg parsing ########
165
166
167 $db = db_init
168 state = nil
169 WHITELIST_CUTOFF = NOW - 4*DAY
170
171
172 ####### begin: state for ips we've seen before #######
173 [[5, 60], # 1 min
174 [10, 60*5], # 5 min
175 [20, 60*60], # 60 min
176 [30, 60*60*24], # 1 day
177 [60, 60*60*24*7]] # 1 week
178 .each do |max_posts, date|
179
180 if $db.execute(<<-SQL, [NOW - date])[0][0] > max_posts
181 select count(*) from c
182 where date > ? and ip = '#{IP}'
183 SQL
184 state = 'rate_limited'
185 end
186 end
187
188 state ||= 'suspect' if $db.execute(<<-SQL)[0][0] > 0
189 select count(*) from c
190 where ip = '#{IP}' and (
191 state = 'banned' or
192 state = 'rate_limited')
193 SQL
194
195 unless state
196 older_date = NOW - DAY*2
197 last_moderated = $db.execute(<<-SQL, [older_date])[-1][0]
198 select date from c
199 where ip = '#{IP}' and (
200 state = 'moderated' or
201 (date < ? and (state = 'timed' or state = 'known')))
202 SQL
203 last_good = $db.execute(<<-SQL, [older_date])[-1][0]
204 select date from c
205 where ip = '#{IP}' and (
206 state = 'picked' or
207 (date < ? and (state = 'timed' or state = 'known')))
208 SQL
209 if last_moderated && last_good
210 if last_good > last_moderated
211 state = 'known'
212 else
213 # these 2 waiting conditions are not actually needed,
214 # since waiting is the default, but meh.
215 state = 'waiting'
216 end
217 elsif last_moderated
218 state = 'waiting'
219 elsif last_good
220 state = 'known'
221 end
222 end
223 ####### end: state for ips we've seen before #######
224
225 ####### begin: whitelist checking #########
226 glob = "../blog/#{'?'*'YYYY-MM-DD-'.length}#{bn GOTO, '.*'}.md"
227 md_file = Dir[glob][0]
228 unless state
229 b = bn(md_file,'.*')
230 post_date = Time.parse(b[0..DATE_LEN]).to_i
231 if post_date > WHITELIST_CUTOFF
232 state = 'timed'
233 end
234 end
235 ###### end: whitelist checking ########
236
237 state ||= 'waiting'
238
239 if state != 'known' && ! captchad
240 do_captcha
241 end
242
243
244 # states:
245 # timed
246 # # was posted a whitelist period, so automatically posted.
247 # # whitelist periods are per page times when legit comments are
248 # # much more likely than spam, so we automatically let comments through.
249
250 # known
251 # # ip posted good comment before: either, one in picked state, or
252 # # a timed/known comment which is over 2 days old (I saw it and didn't remove
253 # # it)
254
255 # picked
256 # # manually marked as a good comment, so publish it.
257
258 # rate_limited
259 # # posting too much, consider them a spammer.
260
261 # moderated
262 # # bad comment, but don't ban them
263
264 # banned
265 # # all comments from this ip dead, new comment's dont even go into the db.
266
267 # waiting
268 # # waiting for manual moderation, get's posted automatically if there
269 # # is none in 24 hours
270
271 # suspect
272 # # had a bad post in the past. does not
273 # # automatically get posted in time without moderation.
274
275
276 # any of the manual states
277 date = $db.execute(<<-SQL)[0][0]
278 select max(date) from c where
279 state = 'moderated' or
280 state = 'banned' or
281 state = 'picked'
282 SQL
283
284 # not the bad automatic states
285 query = <<-SQL
286 select count(*) from c where
287 state != 'rate_limited' and
288 state != 'suspect'
289 SQL
290
291
292 if date
293 new_count = $db.execute(query + 'and date > ?',date)
294 else
295 new_count = $db.execute(query)
296 end
297
298 if new_count == 1
299 require 'net/smtp'
300 def send_email(opts={})
301 opts[:to] ||= ENV['USER']
302 opts[:server] ||= 'localhost'
303 opts[:from] ||= ENV['USER']
304 opts[:from_alias] ||= ENV['USER']
305 opts[:subject] ||= "test subject"
306 opts[:body] ||= ""
307
308 msg = <<END_OF_MESSAGE
309 From: #{opts[:from_alias]} <#{opts[:from]}>
310 To: <#{opts[:to]}>
311 Subject: #{opts[:subject]}
312
313 #{opts[:body]}
314 END_OF_MESSAGE
315
316 Net::SMTP.start(opts[:server]) do |smtp|
317 smtp.send_message msg, opts[:from], opts[:to]
318 end
319 end
320 send_email :subject => 'new comments on iankelling.org'
321 end
322
323 $db.execute('insert into c values (NULL, ?, ?, ?, ?, ?)',
324 [state,
325 IP,
326 NOW,
327 GOTO,
328 COMMENT_TXT])
329
330 post(md_file)
331
332 redir