Newer
Older
WebSocketSample / otp / userdb.rb
@HIROSE Yuuji HIROSE Yuuji on 25 Oct 2022 5 KB Typo
# coding: utf-8
class UserDB
  def initialize(db = "db/otp.sq3")
    @from = MAILFROM
    @expire_after = '+5 minutes'
    @db = SQLite3::Database.new(db)
    @db.execute_batch(<<~EOF)
	CREATE TABLE IF NOT EXISTS users(
          user TEXT PRIMARY KEY,
          gecos TEXT,
          last DATETIME,
          expire DATETIME
	);
	CREATE TABLE IF NOT EXISTS session(
          user TEXT,
          keytype TEXT,		-- 'tmpkey' or 'sesskey'
          key TEXT,		-- key itself
          expire DATETIME,	-- expiration date-time
          UNIQUE(user, keytype, key)
        );
        CREATE TABLE IF NOT EXISTS property(
          user TEXT,
          prop TEXT,
          val  TEXT,
          time DATETIME,
          FOREIGN KEY(user) REFERENCES users(user)
          ON UPDATE CASCADE ON DELETE CASCADE
        );
        PRAGMA FOREIGN_KEYS=on;
	EOF
    @db
  end
  def sendmail(rcpt, subj, body)
    # INPUT:	rcpt = string or array of to-addresses
    #		subj = Subject header of email
    #		body = Message body
    require 'nkf'
    rcpt = rcpt.join(" ") if rcpt.is_a?(Array)
    sj = NKF.nkf('-jM', subj)
    body = NKF.nkf('-j', body)
    ENV["PATH"] = "/bin:/usr/bin:/usr/sbin:/usr/local/bin "
    open("| sendmail -f #{@from} #{rcpt}", "w") do |m|
      m.puts(<<~EOF)
        Subject: #{sj}
        From: OTPsample Admin <#{@from}>
        Mime-Version: 1.0
        Content-type: text/plain; charset=iso-2022-jp

        EOF
      m.puts(body)
    end
  end
  def sendPasscode(email)
    if /.+@.+\..+/ =~ email
      passcode = sprintf("%04d", rand(10000))
      sendmail(email, "OTP: passcode", "Your passcode of OTP = #{passcode}")
      passcode
    end
  end
  def expire
    @db.execute("DELETE FROM session WHERE expire<datetime('now','localtime')")
  end
  def genKey(n=50)
    rand(10**n).to_s(36)		# 10^50の乱数の36進数表記
  end
  def genSkey(user)
    expire				# まず時間切れのキーを消す
    sKey = genKey
    @db.execute("INSERT INTO session VALUES(?, 'sesskey', ?,
	datetime('now', 'localtime', '#{@expire_after}'));", user, sKey)
    sKey
  end
  def genTmpkey(user)
    passcode = sendPasscode(user)
    STDERR.puts "passcode: #{passcode}"
    tmpkey   = genKey
    joined   = tmpkey + "-" + passcode
    @db.execute("INSERT INTO session VALUES(?, 'tmpkey', ?,
	datetime('now', 'localtime', '#{@expire_after}'));", user, joined)
    tmpkey
  end
  def confirmUserAdd(user)
    r = @db.execute("SELECT user from users WHERE user=?", user)
    if !r[0]
      @db.execute("INSERT INTO users(user) VALUES(?)", user)
    end
  end
  def authTmpKey(user, tmpkey, code)	# tmpkeyとパスコードの組み合わせで認証
    expire				# 先に期限切れのキーを消す
    joined = tmpkey + "-" + code
    r = @db.execute(
      "SELECT user FROM session WHERE user=? AND keytype='tmpkey'
	AND key=?", user, joined)
    if r[0] && r[0].length==1
      # このように認証が通ったときは単に true を返すのではなく
      # その後の処理に使い回せる値を返す(updateSkeyも参照)
      updateSkey(user, genKey)
    end
  end
  def authSkey(user, skey)		# ブラウザに残るセッションキーで認証
    expire
    r = @db.execute(
      "SELECT user FROM session WHERE user=? AND keytype='sesskey' AND key=?",
      user, skey)
    if r[0] && r[0].length==1
      r = @db.execute("SELECT user FROM users WHERE user=?", user)
      expire = "datetime('now', 'localtime', '+50 days')"
      if r[0]                   # 既にユーザがいる場合はUPDATE
        @db.execute(<<~EOF, user)
	  UPDATE users
	   SET last=datetime('now', 'localtime'), expire=#{expire}
         WHERE user=?
	EOF
      else
        # 既存ユーザに対して REPLACE INTO を使うと
        # 一瞬削除されるのでFOREIGN KEY制約で関連データが消える
        @db.execute(<<~EOF, user)
	INSERT INTO users(user, last, expire) VALUES(?,
	  datetime('now', 'localtime'),
	  datetime('now', 'localtime', '+50 days'))
	EOF
      end
      updateSkey(user, skey)
    end
  end
  def updateSkey(user, sKey)
    expire				# まず時間切れのキーを消す
    confirmUserAdd(user)
    @db.execute("REPLACE INTO session VALUES(?, 'sesskey', ?,
	datetime('now', 'localtime', '#{@expire_after}'));", user, sKey)
    addProps(user, "login", Time.now.strftime("%F %T"))
    sKey	# updateした値そのものを返すことで呼び主が次の処理に使える
  end
  # ここから下のメソッドは遊びチャットなので無視してよい
  def addProps(user, prop, val)
    @db.execute(<<~EOF, user, prop, val)
	INSERT INTO property VALUES(?, ?, ?, datetime('now', 'localtime'))
	EOF
  end
  def countProps(user, prop, val)
    r = @db.execute(<<~EOF, user, prop, val)
	SELECT count(*) FROM property
	WHERE user=? AND prop=? AND val=?
	EOF
    # p r[0]
    (r[0] && r[0][0].to_i > 0) ? r[0][0].to_i : 0 # count>1ならそれ さもなくば0
  end
  def notAI(hash, user)         # これも遊びメソッドなので無視してよい
    if (msg = hash["note"])
      addProps(user, "note", msg)
      if msg == "?"
        r = @db.execute("SELECT COUNT(*) FROM property WHERE user=?", user)
        r[0] and return sprintf("主に会うのは%d度目じゃ", r[0][0].to_i)
      end
      case countProps(user, "note", msg)
      when 1
        %w(ほう なるほど ふむ それは? わからぬ しらぬ おっぉう はて
        ああ 御意 まて そうか だな んだす んぐ ... まじか え、
        初耳だ それな けしからぬ).sample
      when 5, 10
        "しつこい"
      when 4
        msg+"が好きなようじゃな。もっと深く語ってくれてもよいぞ。"
      when 2..11
        "それは聞いた"
      else
        "暇なのか? わしは暇だ"
      end
    end
  end
end