Scheme:CHICKEN:サイト独自のいいねボタンを作る
FacebookなどSNSのアカウントがない人でも「いいね」できるように、独自の「いいネ!」ボタンを作った。
データはsqliteに保存するので、CUIならslite3で、どの記事に「いいネ!」が多いか、いつ「いいネ!」されたかぐらいは集計できる。アカウント登録とかは、もちろんないので、どこの誰が「いいネ!」したかは分からない。
SNSやってない、という人が気軽に参加できるのが「いいネ!」。
「いいネ!」ボタンの処理をするscheme (CHICKEN) cgiスクリプト
DBにはsqliteを使う。sqliteは導入が手軽なので、軽い処理のときにいい。
「いいネ!」されたページを特定するために、HTTP_REFERERを使う。Cookieを利用して、同じユーザが24時間は連続いいねできないようにする。
CHICKENにはcgiスクリプト用のライブラリがない(?)ので、ローレベルの記述が増えるけど、cgiってこういうことしてるんだというのが分かる。
#! /bin/sh
#|
exec csi -s "$0" "$@"
|#
(cond-expand
(chicken-4
(use irregex ;irregex-replace
sql-de-lite
uri-common; uri-decode-string
srfi-19;date time @cookie
simple-md5;string->md5sum @cookie
))
(chicken-5
(import (chicken string) ;conc
(chicken irregex) ;irregex-replace
sql-de-lite
uri-common; uri-decode-string
srfi-19;date time @cookie
simple-md5;string->md5sum @cookie
)))
; CGI library start
; a=A&b=B -> ((a . A) (b . B))
(define ($.query-string->alist string #!optional (delimiter "&") (separator "= "))
(let loop ((params (string-split (if (string? string) string "") delimiter))
(result '()))
(if (null? params)
(reverse result)
(loop (cdr params)
(let ((q (string-split (car params) separator)))
(cons (cons (car q) (uri-decode-string (cadr q))) result))))))
; GET POST 共通のクロージャ手続き
; 引数1個の時は検索
; 引数2個の時は更新か追加
(define $
;; 環境:読み込みの際だけ評価される
(let ((alist (append
;; GETデータ
($.query-string->alist (get-environment-variable "QUERY_STRING"))
;; POSTデータを標準入力から取得する
;; read系の手続きは実行するたびに、ポート内の位置が進むことに注意
(if (equal? (get-environment-variable "REQUEST_METHOD") "POST")
($.query-string->alist
(read-string (string->number (get-environment-variable "CONTENT_LENGTH"))))
'()))))
;; 手続き本体が呼ばれる際、環境の初期化は行わない
(lambda (k #!optional v)
(if v (alist-update! k v alist equal?))
(alist-ref k alist equal?))))
(define $.cookies
(let ((alist ($.query-string->alist (get-environment-variable "HTTP_COOKIE") ";")))
(lambda (k) (alist-ref k alist equal?))))
(define ($.prepare-cookie k v)
(conc
"Set-Cookie:" k "=" v ";"
"Max-Age=" (* 60 60 24) ";";one iine for 24hrs
"Path=/;" ;uniqe per a browser
"SameSite=Lax;secure"))
; またもやクロージャ
; ($.cgi-header "hoge") => '("hoge" "Content-Type...")内部リストに"hoge"追加
; ($.cgi-header) => 内部リストを順番にprint
(define $.cgi-header
(let ((lines '("Content-Type: text/html; charset=UTF-8")))
(lambda (#!optional v)
(cond (v (set! lines (cons v lines)))
(else
(for-each print lines)
(newline))))))
; CGI library end
(let* ((db (open-database "iine.db"))
(mode ($ "mode"))
(pid (irregex-replace "\\?.*$|#.*$" (get-environment-variable "HTTP_REFERER") ""));; ?や#以下は削除
(uid ($.cookies "iine-uid"))
)
(define display-count
(lambda () (display (query
fetch-value
(sql db "SELECT COUNT(*) FROM iine_table WHERE pid = ?")
pid))))
;; create table
(exec (sql db
"CREATE TABLE IF NOT EXISTS iine_table
(
id integer primary key not null,
pid integer not null,
uid text not null,
created timestamp not null default (datetime('now', 'localtime'))
);"))
(exec (sql db "PRAGMA synchronous = 0"))
;; Cookieがなければ準備
(unless uid
(set! uid (string->md5sum (date->string (current-date) "~Y-~m-~d ~H:~M:~S~N")))
($.cgi-header ($.prepare-cookie "iine-uid" uid)))
;; cgi headerの出力
;; 応答の最初に行うこと
($.cgi-header)
(cond
;; ページ読み込み時の処理
((and (equal? mode "get") pid) (display-count))
;; いいネ!時の処理
((and (equal? mode "put") pid uid)
;;いいネ!してなかったら
(if (= 0 (query fetch-value
(sql db "SELECT COUNT(*) FROM iine_table WHERE pid = ? AND uid = ?")
pid uid))
;; いいネする
(exec (sql db "INSERT INTO iine_table (pid, uid) VALUES (?, ?)")
pid uid))
(display-count))
(else ""))
(close-database db)
)
「いいネ!」ボタン表示用のsxml
Hydeで使うことを前提にしてるので、htmlはsxml形式で。
`(div (@ (class "iine") (id "iine"))
(p (@ (class "iine-lead")) "この記事が気に入ったら →")
(a (@ (href "#") (class "iine-button"))
(span (@ (class "icon")) (i (@ (class "fa fa-heart"))) "いいネ!")
(span (@ (class "number")))))
ブラウザからcgiスクリプトを呼び出すjavascript
// iine.js
$(document).ready(function(){
$('.iine-button').each(function(){
var $iine = $(this);
// 表示処理
$.ajax({
type: 'GET',
url: '/iine/?mode=get',
cache: false,
success: function(data){
$iine.find('.number').text(data);
}
});
// 追加処理
$iine.click(function(){
$.ajax({
type: 'POST',
url: '/iine/?mode=put',
cache: false,
success: function(data){
$iine.find('.number').text(data);
}
});
return(false);
});
});
});
ボタン用のcss
.iine {
margin-top: 2em;
border: #b0b0b0 1px solid;
padding: 0.5em;
}
.iine .iine-lead {
display: table-cell;
padding-right: 0.5em;
text-indent: 0;
}
.iine .iine-button {
display: table-cell;
}
.iine .iine-button .icon {
}
.iine .iine-button .number {
position: relative;
border: #278dbf 1px solid;
border-radius: 20%;
margin: 0.5em;
padding: 0 5px;
}
/* https://cssarrowplease.com */
.iine .iine-button .number:after, .iine .iine-button .number:before {
right: 100%;
top: 50%;
border: solid transparent;
content: "";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.iine .iine-button .number:after {
border-color: #rgba(255, 255, 255, 0);
border-right-color: #ffffff;
border-width: 4px;
margin-top: -4px;
}
.iine .iine-button .number:before {
border-color: rgba(155, 180, 196, 0);
border-right-color: #278dbf;
border-width: 5px;
margin-top: -5px;
}