kobapan@wiLiki
Login
Scheme:CHICKEN

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;
}

参考

Debian
Emacs
Firefox
Gauche
JavaScript
Linux
Scheme
org-mode/latex

More ...