Newer
Older
jstrr / jtserv.rb
@HIROSE Yuuji HIROSE Yuuji on 20 Nov 2021 15 KB jsTrr Initial Commit
#!/usr/bin/env ruby
# coding: utf-8
require 'em-websocket'
require 'json'
require 'csv'
require 'sqlite3'

PORT	= 8814

# Interchange of Information is done in JSON form.
# (IN)	{"cmd": Command, OtherArgs...}
# (OUT)
#
# Command is one of:
#	team
#	text


class UserDB
  def initialize()
    @db = SQLite3::Database.new("users.sq3")
    ### @db.results_as_hash = true
    @expireskey = '+3 days'
    @expiretmpkey = '+1 hour'
    @textlist = IO.readlines("TEXT")
    @from = 'postmaster@koeki-prj.org'
    @rank = %w(必須 標準 目標 一流 超一流 頂点)
    dbinit
  end
  def dbinit()
    # "at" field is like: YYYY-MM-DDThh:mm:ss at finish time
    @db.execute_batch(<<~EOF)
        -- DROP TABLE score;
	CREATE TABLE IF NOT EXISTS score(
	  user, at, text,
          step INTEGER, score INTEGER, types INTEGER,
          miss INTEGER, time INTEGER,
          UNIQUE(user, at),
          FOREIGN KEY(user) REFERENCES users(user)
          ON DELETE CASCADE ON UPDATE CASCADE
	);
	EOF
  end
  def email(user)
    r = @db.execute("SELECT email FROM users WHERE user = ?", user)
    if r[0] && /.+@.+/ =~ r[0][0] then
      r[0][0]
    else
      nil
    end
  end
  def sendmail(rcpt, subj, body)
    require 'nkf'
    rcpt = rcpt.join(" ") if rcpt.is_a?(Array)
    sj = NKF.nkf('-jM', subj)
    body = NKF.nkf('-j', body)

    open("| sendmail -f#{@from} #{rcpt}", "w") do |m|
      m.puts(<<~EOF)
	Subject: #{sj}
	From: jsTrr Admin <#{@from}>
	Mime-Version: 1.0
	Content-type: text/plain; charset=iso-2022-jp

	EOF
      m.puts(body)
    end
  end
  def genToken(user, email)
    d1 = ("a".."z").to_a.sample
    d2 = ("a".."z").to_a.sample
    token = d1 + d2 + sprintf("%02d", rand(100))
    sendmail(email, 'jsTrr: passcode',
             "Your(%s) passcode = %s\n" % [user, token])
    token
  end
  def genKey(n=50)
    rand(10**n).to_s(36)
  end
  def genSkey(user)
    # First, erase expired session keys
    @db.execute("DELETE FROM skey WHERE expire < datetime('now','localtime')")
    sKey = genKey
    @db.execute("INSERT INTO skey VALUES(?, ?,
       datetime('now', 'localtime', '#{@expireskey}'));", user, sKey)
    sKey
  end
  def storeTmpKey(user, token)
    # First, erase expired session keys
    @db.execute("DELETE FROM tmpkey
	 WHERE expire < datetime('now','localtime')")
    tmpkey = genKey
    @db.execute("INSERT INTO tmpkey
     VALUES(?, ?, ?, datetime('now', 'localtime', '#{@expiretmpkey}'));",
                user, tmpkey, token)
    tmpkey
  end
  def authTmpKey(user, tmpkey, token)
    r = @db.execute("SELECT user FROM tmpkey
		WHERE user=? AND tmpkey=? AND token=?", user, tmpkey, token)
    p token
    p r[0]
    if r && r[0] && r[0].length==1
      # Auth OK!
      genSkey(user)
    end
  end
  def updateSkey(user, skey)
    @db.execute("UPDATE skey
		 SET expire=datetime('now', 'localtime', '#{@expireskey}')
		 WHERE user=? AND skey=?",
                user, skey)
    skey
  end
  def authKey(user, skey)
    r = @db.execute("SELECT user FROM skey
		WHERE user=? AND skey=?", user, skey)
    if r && r[0] && r[0].length==1
      # Auth OK!
      updateSkey(user, skey)
    end
  end
  def userScoreInfo(user)
    rval = Hash.new
    @db.execute("SELECT text, max(score), max(step), count(user) FROM score
                 WHERE user=? GROUP BY user, text;", user) do |row|
      rval[row[0]] = {"highscore" => row[1].to_i, "step" => row[2].to_i,
                      "trial" => row[3].to_i}
    end
    if r=@db.execute("SELECT gecos FROM users WHERE user=?", user)
      rval["gecos"] = r[0][0]
    end
    steptrial = Hash.new{|k, v| k[v]=Hash.new}
    @db.execute("SELECT text, step, count(step) FROM score
                WHERE user=? GROUP BY user, text, step;", user) do |row|
      txt, step, trial = row
      steptrial[txt][step] = trial
    end
    rval["steptrial"] = steptrial
    p rval
    rval
  end
  ################### Trr STEP Calculation ###################
  def msgBeginner(s)
    if s <= 0
      "0点というのは問題だわ。これからかなりの努力が必要よ。道のりは長いけど頑張りましょう。"
    elsif s <40
      "少なくとも英文字ぐらいは絶対覚えること。業界必須の100点に向けてこれから頑張りましょう。"
    elsif s < 80
      "キー配置も大分覚えたようだけどまだまだだわ。業界必須の100点に向けてこれから頑張りましょう。"
    elsif s < 130
      "基礎的な技術は身に付けているようだけどまだまだだわ。業界標準の200点に向けてこれから頑張りましょう。"
    elsif s < 180
      "なかなかの実力ね。でもスピードと正確さが少し足りないわ。業界標準の200点に向けてもう少し頑張りましょう。"
    elsif s < 280
      "なかなかやるわね。もう少し頑張れば業界目標の300点をきっと突破できるわ。"
    elsif s < 380
      "すごいわね。初めてでこれぐらい出せれば十分だわ。でも業界一流の400点に向けてもう少し頑張りましょう。"
    elsif s < 480
      "すっごい!こんな点を出す人は滅多にいないわよ。ひょっとしてプロではないかしら?"
    else
      "あまりにも超人的だわ。きっとギネスブックに載るわよ。"
    end
  end
  def msgForRecordBreaker(s)
    rank = (s/100).to_i
    sxx  = s%100
STDERR.printf("s=%d, rank=%d, sxx=%d\n", s, rank, sxx)
    if s < 600
      sprintf("業界%sの%d点%s。",
              @rank[rank], rank*100+100,
              sxx < 67 ? "目指して頑張って" : "までもうすぐよ")
    else
      "よくここまでやるわね。あなたの目標は一体何なの?"
    end
  end
  def msgSpecialForRecordBreaker(s)
    rank = (s/100).to_i
    sprintf("ついに業界%sの%d点突破ね!これからは業界%sの%d点を目指して頑張りましょう。",
            @rank[rank], rank*100+100,
            @rank[rank+1], rank*100+200)
  end
  def msgSuccess(user, step, highscore, score)
    diff = [highscore - score, 0].max
    trial = 0
    if r=@db.execute("SELECT count(score) FROM score
                      WHERE user=? AND step=? GROUP BY user, step",
                     user, step)
      trial = r[0][0].to_i
    end
    STDERR.printf("Trial=%d\n", trial)
    if (score-step*10) > 100
      "あなたには簡単すぎたようね。"
    else
      m = %w(軽く突破したわね。 わりと簡単に突破したわね。
             ちょっと手こずったようね。 だいぶ手こずったようね。
             よく頑張ったわね。 随分苦労したようね。 苦しみ抜いたわね。)
      m[[Math.log2(trial)-1, m.length-1].min]
    end
  end
  def msgFail(diff, highscore, score, missratio)
    if diff<10
      "あとほんの少しだったのに...本当に惜しかったわね。"
    elsif diff<20
      "惜しかったわね。"
    elsif diff<30
      "その調子よ。"
    elsif diff<40
      "もう一息だわ。でも息抜きはだめよ。。"
    elsif diff<50
      "頑張ればきっとできるわ。"
    elsif diff < 60
      "努力あるのみよ。"
    elsif diff < 100 || missratio > 30
      if missratio > 60
        "練習に練習を重ねなさい。"
      elsif missratio > 40
        "ミスが多過ぎるわ。初心に帰って一つ一つ慎重に打つ練習をしなさい。"
      elsif missratio > 24
        "ミスが多いわ。正確に打つ練習をしなさい。"
      elsif missratio > 8
        "練習に練習を重ねなさい。"
      else
        "正確に打ってるようだけどスピードが遅すぎるわ。速く打つ練習に励みなさい。"
      end
    elsif diff < 110
      "「trrの道は一日にしてならず」"
    elsif diff < 120
      "「trrに王道なし」"
    elsif diff < 130
      "あらまぁ。%d点を出した人がたったの%d点なんていったいどうしたのよ。" \
      % [highscore, score]
    elsif diff < 140
      "%d点はまぐれだったの?" % [highscore]
    else
      "あなたの実力ってこの程度だったのね。"
    end
  end
  def evalStep(user, h)	# h == Hash from client
    # Return the Hash whose keys are "step", "message", "time"
    highscore = step = 0
    update =   nil	# Break Hiscore
    pass =     nil	# Go to next step
    beginner = nil	# Beginner flag
    time  = h["finish"] - h["start"]
    types = h["types"].to_i
    miss  = h["miss"].to_i
    text  = h["text"]
    speed = (60*types/time).to_i
    score = [(60*(types-miss)/time).to_i, 0].max
    missratio  = 1000*h["miss"].to_i/h["types"].to_i	# 1/1000!!!
    r = @db.execute("SELECT max(score) ms,max(step) mst
		     FROM score WHERE user=? AND text=?
                     GROUP BY user, text", user, text)
    if r && r[0] && r[0][0]
      highscore, step = r[0]
      if score >= step*10	# Qualify current step
        step += 1; pass = true
      end
      update = (score > highscore)
    else
      step = score/10; beginner = true
    end
    if time <= 1
      return {"step" => h["step"],
              "message" => "ごめんなさい。時間計るの忘れてたわ。"}
    end
    # p score
    # user, at, text, step, score, types, miss, time
    @db.execute(<<~EOF, user, text, step, score, types, miss, time)
	REPLACE INTO score VALUES(
          ?, datetime('now', 'localtime'), ?, ?, ?, ?, ?, ?);
	EOF
    diff = step*10 - score
    recbreaker = (score > highscore && highscore/100 != score/100)
    msg = if score > 750
            "そんなことでいいの? 恥を知りなさい。"
          elsif beginner
            msgBeginner(score)
          elsif pass               # STEP clear!
            sprintf("ステップ%d突破%sおめでとう!\n%s",
                    step,
                    update ? "そして記録更新" : "",
                    recbreaker ?
                      msgSpecialForRecordBreaker(highscore)
                    : msgForRecordBreaker(highscore)
                   )
            
          elsif update
            "記録更新おめでとう!\n"+msgSuccess(user, step, highscore, score)
          else
            msgFail(diff, highscore, score, missratio)
          end
    rv = {"score" => score, "step" => step, "time" => time.round(1),
          "speed" => speed, "message" => msg, "types"=>types,
          "highscore" => [highscore, score].max}
    if false
      # Peek trial count of this level
      r = @db.execute("SELECT count(*) FROM score
                     WHERE user=? AND text=? AND step=?
                     GROUP BY user, text, step;", user, text, step)
      rv["strial"] = r[0][0].to_i
      r = @db.execute("SELECT count(*) FROM score
                     WHERE user=? AND text=? GROUP by user, text", user, text)
      rv["trial"] = r[0][0].to_i
    else
      rv["scoreinfo"] = userScoreInfo(user)
    end
    return rv
  end
  def ranking(text)
    @db.execute(<<~EOF, text)
	SELECT user, max(score), max(step), count(score),
               cast(round(sum(time)/60) as INT), max(at)
        FROM score WHERE text=? GROUP BY user;
	EOF
  end
end

db = UserDB.new
connections = {}
team = Hash.new{|h,k| h[k]=Hash.new}
broadcast = lambda{|hash|
  printf("sending %s\n", hash)
  connections.keys.each do |c|
    c.send(JSON.generate(hash))
  end
}
EM::WebSocket.start({:host => "0.0.0.0", :port => PORT}) do |ws_conn|
  Thread.new {
    while cmd = gets
      case cmd.chop
      when 's'
        broadcast.call({start: Time.now.to_f})
      end
    end
  }
  ws_conn.onopen do
    connections[ws_conn] = {}
    ws_conn.send(JSON.generate({myid: connections.length}))
    STDERR.printf("OP: %d clients\n", connections.length)
  end
  ws_conn.onclose do
    connections.delete(ws_conn)
    STDERR.printf("CL: %d clients\n", connections.length)
  end
  ws_conn.onmessage do |msg|
    STDERR.printf("RECV: [%s]\n", msg)
    begin
      json = JSON.parse(msg)
    rescue
      return
    end
    userinfo = connections[ws_conn]
    if userinfo["skey"] then
      if json["team"] then
        #team[json["team"]][json["name"]] ||= nil
        userinfo = {"team" => json["team"], "name" => json["name"]}
      elsif json["settext"]
        # {"松" => {"name1"=>1, "name2" => 2}}
        tm = userinfo["team"]
        nm = userinfo["name"]
        team[tm][nm] = json["text"]
        connections.each do |k, v|
          p v["team"]
          next unless tm == v["team"]	# Skip other team
          next if ws_conn==k		# Skip myself
          k.send(JSON.generate({"disable" => json["text"]}))
        end
        userinfo["text"] = json["text"].to_i
        si = {"scoreinfo" => db.userScoreInfo(userinfo["user"])}
        ws_conn.send(JSON.generate(si))
      elsif json["finish"]
        user = userinfo["user"]
        if user && user > ""
          if false
            ## start  = # json["start"].to_i
            start  = userinfo["start"]
            ## finish = json["finish"].to_i
            finish = Time.now.to_f
            types  = json["types"].to_i
            STDERR.printf("types=%d", types)
            miss   = json["miss"].to_i
            text   = json["text"]
            time   = finish-start
            score  = 60*(types - 10*miss)/time
          end
          json["start"] = userinfo["start"]
          json["finish"] = Time.now.to_f
STDERR.printf("Start=%.2f, Finish=%.2f\n", json["start"], json["finish"])
          eval  = db.evalStep(user, json)
          ws_conn.send(JSON.generate(eval))
          userinfo.delete("typelist")
        end
      elsif json["types"]
        unless userinfo["typelist"]
          p "START!"
          userinfo["start"] = Time.now.to_f
          userinfo["typelist"] = Hash.new
        end
        userinfo["typelist"][json["types"]] = Time.now.to_f
      elsif json["ranking"]
        text = json["ranking"]
        p rank=db.ranking(text)
        ws_conn.send(JSON.generate({"ranking": db.ranking(text)}))
      end
    else ########## Before authenticated
      if json["Login"]
        user = json["Login"]
        email = db.email(user)
        token = db.genToken(user, email)
        tmpkey  = db.storeTmpKey(user, token)
        j = JSON.generate({"email" => email, "tmpkey" => tmpkey, "user" => user})
        p token
        ws_conn.send(j)
      elsif json["skey"]
        user, skey = json["user"], json["skey"]
        skey = db.authKey(user, json["skey"])
        STDERR.printf("SKey=%s", skey)
        userinfo["skey"] = skey	# Cache skey
        STDERR.printf("CACHE skey for user %s\n", user)
        userinfo["user"] = user	# Cache User
        if skey
          si = db.userScoreInfo(user)
        STDERR.printf("scoreinfo = %s\n", si)
          j = JSON.generate({"user"=>user, "skey"=>skey, "scoreinfo"=>si})
          ws_conn.send(j)		# Login success by skey
        else
          ws_conn.send(JSON.generate({"fail" => "nokey"}))
        end
      elsif json["Passcode"]
        token = json["Passcode"]
        tmpkey = json["tmpkey"]
        user = json["user"]
        authkey = db.authTmpKey(user, tmpkey, token)
        STDERR.printf("authkey=%s", authkey)
        if authkey
          userinfo["skey"] = authkey	# Cache skey
          STDERR.printf("Cache skey for user %s\n", user)
          userinfo["user"] = user	# Cache User
          si = db.userScoreInfo(user)
          j = JSON.generate({"user"=>user, "skey"=>authkey, "scoreinfo"=>si})
          ws_conn.send(j)		# Login success by token+tmpkey
        else
          j = JSON.generate({"user" => user, "fail" => "fail"})
          ws_conn.send(j)		# Login failure by token
        end
      else
        ws_conn.send("No such user")
        ws_conn.close           	# Force guest out
      end
    # broadcast.call({"message" => msg})
    end
  end
end