Python配合Redis寫個Remember小工具

菩提樹下的煮茶小童子發表於2019-02-26
禪心 · 悟道
禪心 · 悟道

前言

Mac上有一個自帶的備忘錄,感覺還挺好用的。然後也想自己動手,做個類似的Remember小工具來玩一下。

工具型別:胖服務端,瘦客戶端的模式。大致的場景就是客戶端只管把自己想讓被提醒的事項發給伺服器端,然後配合自己的本地掃描,對符合要求的memo進行彈框提醒。

最近對Redis比較著迷一點,被其優雅高效的設計所打動。雖然對於搜尋方面支援的不太好,但是搜尋的話使用專業的搜尋服務就好了。我個人比較崇尚Unix工具系的宗旨:一個工具只專注於做一件事,這也是Redis目前所體現的。

伺服器端

對於伺服器端的設計的初衷,是一個“胖胖的”,做了大部分的工作的形式,但是做著做著,發現客戶端做的工作其實也蠻多的。目前伺服器端的任務是:

  • 接受使用者註冊
  • 對memo支援CRUD
  • 對請求進行“安全甄選”

目錄結構

➜  server ll *.py
-rw-r--r--  1 changba164  staff   346B Oct 28 14:53 datastructor.py # 常見資料結構bean
-rw-r--r--  1 changba164  staff   2.6K Oct 28 18:22 redishelper.py  # redis操作相關的工具類
-rw-r--r--  1 changba164  staff   4.6K Oct 28 18:42 server.py   # 對客戶端提供服務支援複製程式碼

程式碼比較簡單,下面簡單來看下具體的內容。

datastructor.py

#!/usr/bin python
# coding: utf8
# file: .py

import sys

reload(sys)
sys.setdefaultencoding(`utf8`)
import json

class ResponseCode(object):
    """
    服務響應碼
    """
    def __init__(self, code, msg):
        self.code = code
        self.msg = msg

    def getcode(self):
        return json.dumps({"code":self.code, "msg":self.msg})複製程式碼

redishelper.py

#!/usr/bin python
# coding: utf8
# file: .py

import sys

reload(sys)
sys.setdefaultencoding(`utf8`)

import redis
import hashlib
import uuid
import time

class RedisHelper(object):
    """
    redis操作相關的工具類。
    """
    def __init__(self):
        self.rs = redis.Redis(host="localhost", port=6379, db=3, encoding="utf8", charset="utf8")
        # 相關key
        self.uids = "userids:set"
        self.rank = "unfinished:zset:"
        self.unfinished = "unfinished:hash:"
        self.finished = "finished:hash:"

    def check_request_valid(self, uid, securitycode):
        if uid is None:
            return False
        if securitycode == hashlib.md5(uid).hexdigest():
            return True
        return False

    def register(self, uid):
        if uid is None:
            return False
        self.rs.sadd(self.uids, uid)
        return True

    def check_user(self, uid):
        if uid is None:
            return False
        return True if self.rs.sismember(self.uids, uid) else False

    def add_memo(self, uid, memoid, memo=""):
        if uid is None:
            return False
        memoid = memoid
        self.rs.sadd(str(uid), memoid)
        self.rs.hset(self.unfinished+str(uid), memoid, memo)
        self.rs.zadd(self.rank+str(uid), memoid, int(memoid))


    def update_memo(self, uid, memoid, memo):
        if uid is None:
            return False
        if not self.rs.sismember(str(uid), memoid):
            return False
        self.rs.hset(self.unfinished+str(uid), memoid, memo)
        return True

    def delete_memo(self, uid, memoid):
        if uid is None:
            return False
        memo = self.rs.hget(self.unfinished+str(uid), memoid)
        self.rs.hset(self.finished+str(uid), memoid, memo)
        self.rs.zrem(self.rank+str(uid), memoid)
        return True

    def get_memo(self, uid, memoid):
        if uid is None:
            return None
        return self.rs.hget(self.unfinished+str(uid), memoid)

    def get_memos(self, uid, reverse=True):
        if uid is None:
            return None
        memoids = self.get_memoids(uid, reverse)
        print memoids
        memos = []
        for item in memoids:
            memos.append(self.rs.hget(self.unfinished+str(uid), item[0]))
        return memos

    def get_memoids(self, uid, reverse=True):
        if uid is None:
            return []
        if reverse == True:
            return [item[0] for item in self.rs.zrevrange(self.rank+str(uid), 0, -1, withscores=True)]
        else:
            return [item[0] for item in self.rs.zrange(self.rank+str(uid), 0, -1, withscores=True)]複製程式碼

server.py

#!/usr/bin python
# coding: utf8
# file: .py

import sys

reload(sys)
sys.setdefaultencoding(`utf8`)
from flask import Flask, request
import redishelper
import datastructor
import logging
import json

logging.basicConfig(
    level=logging.INFO,
    format=`%(asctime)s:%(levelname)s %(message)s`,
    datafmt="%Y-%m-%d %H:%i:%s",
    filename="/Users/changba164/guo/tools/remeber/server/memo-server.log",
    filemode="a"
)

app = Flask(__name__)
helper = redishelper.RedisHelper()


@app.route("/", methods=[`GET`, `POST`])
def home():
    return "It works!"

@app.route("/register", methods=["POST"])
def register():
    uid = request.form["uid"]
    helper.register(uid)
    res = datastructor.ResponseCode(1000, "uid={}".format(uid))
    return res.getcode()

@app.route("/add", methods=["POST"])
def add():
    uid = request.form[`uid`]
    securitycode = request.form[`securitycode`]
    memoid = request.form[`memoid`]
    memo = request.form[`memo`]

    isuserexists = helper.check_user(uid)
    if isuserexists == False:
        return datastructor.ResponseCode(1001, "該使用者還未註冊喲!").getcode()
    isvalid = helper.check_request_valid(uid, securitycode)
    logging.info("{}: {}".format(uid, securitycode))
    if isvalid == False:
        return datastructor.ResponseCode(1002, "身份資訊不合法!").getcode()
    helper.add_memo(uid, memoid, memo)
    return datastructor.ResponseCode(1000, "memo已經儲存啦!").getcode()

@app.route("/update", methods=["POST"])
def update():
    uid = request.form[`uid`]
    securitycode = request.form[`securitycode`]
    memoid = request.form[`memoid`]
    memo = request.form[`memo`]

    isuserexists = helper.check_user(uid)
    if isuserexists == False:
        return datastructor.ResponseCode(1001, "該使用者還未註冊喲!").getcode()
    isvalid = helper.check_request_valid(uid, securitycode)
    if isvalid == False:
        return datastructor.ResponseCode(1002, "身份資訊不合法!").getcode()
    helper.update_memo(uid, memoid, memo)
    return datastructor.ResponseCode(1000, "memo已經更新啦!").getcode()

@app.route("/delete", methods=["POST"])
def deletememo():
    uid = request.form[`uid`]
    securitycode = request.form[`securitycode`]
    memoid = request.form[`memoid`]

    isuserexists = helper.check_user(uid)
    if isuserexists == False:
        return datastructor.ResponseCode(1001, "該使用者還未註冊喲!").getcode()
    isvalid = helper.check_request_valid(uid, securitycode)
    if isvalid == False:
        return datastructor.ResponseCode(1002, "身份資訊不合法!").getcode()
    helper.delete_memo(uid, memoid)
    return datastructor.ResponseCode(1000, "memo已經刪除/完成啦!").getcode()

@app.route("/detail", methods=["POST"])
def detail():
    uid = request.form[`uid`]
    securitycode = request.form[`securitycode`]
    memoid = request.form[`memoid`]

    isuserexists = helper.check_user(uid)
    if isuserexists == False:
        return datastructor.ResponseCode(1001, "該使用者還未註冊喲!").getcode()
    isvalid = helper.check_request_valid(uid, securitycode)
    if isvalid == False:
        return datastructor.ResponseCode(1002, "身份資訊不合法!").getcode()
    detail = helper.get_memo(uid, memoid)
    return datastructor.ResponseCode(1000, detail).getcode()

@app.route("/lists", methods=[`POST`])
def lists():
    uid = request.form[`uid`]
    securitycode = request.form[`securitycode`]
    reverse = request.form[`reverse`]
    isuserexists = helper.check_user(uid)
    if isuserexists == False:
        return datastructor.ResponseCode(1001, "該使用者還未註冊喲!").getcode()
    isvalid = helper.check_request_valid(uid, securitycode)
    if isvalid == False:
        return datastructor.ResponseCode(1002, "身份資訊不合法!").getcode()
    memos = helper.get_memos(uid, reverse)
    res = datastructor.ResponseCode(1000, json.dumps((memos))).getcode()
    return res

@app.route("/recent", methods=[`POST`])
def recentids():
    uid = request.form[`uid`]
    securitycode = request.form[`securitycode`]
    isuserexists = helper.check_user(uid)
    if isuserexists == False:
        return datastructor.ResponseCode(1001, "該使用者還未註冊喲!").getcode()
    isvalid = helper.check_request_valid(uid, securitycode)
    if isvalid == False:
        return datastructor.ResponseCode(1002, "身份資訊不合法!").getcode()
    memoid = helper.get_memoids(uid, False)[0]
    result = {
        "memoid": memoid,
        "memo": helper.get_memo(uid, memoid)
    }
    res = datastructor.ResponseCode(1000, json.dumps((result))).getcode()
    return res


if __name__ == `__main__`:
    app.run(host=`localhost`, port=9999, debug=True)複製程式碼

“瘦客戶端”

客戶端所要做的無非是:新增memo,獲得最需要處理的memo,並進行彈窗提示等。目前客戶端的任務還不算完成,也就簡單實現了下。

➜  client ls
testutils.py utils.py
➜  client複製程式碼

utils.py

#!/usr/bin python
# coding: utf8
# file: .py

import sys

reload(sys)
sys.setdefaultencoding(`utf8`)

import requests
import json
import hashlib
import time
import datetime
import tkMessageBox
from Tkinter import *

def get_security_code(uid):
    return hashlib.md5(uid).hexdigest()

def time_to_stamp(seed):
    """
    將白話點的時間轉換為時間戳,格式為: 00:00:00:00 天:小時:分鐘:秒
    """
    curtime = int(time.time())
    d, h, m, s = (int(item) for item in str(seed).split(":"))
    targettime = curtime + (d*86400)+ (h*3600)+(m*60)+s
    return targettime

def stamp_to_time(stamp):
    return datetime.datetime.utcfromtimestamp(float(stamp))

def make_dialog(timestr, msg):
    tkMessageBox.showinfo(title=timestr, message=msg)
    # root = Tk()
    # root.title(timestr)
    # frame = Frame(root)
    # Label(frame, text=msg).pack()
    # frame.pack(side=TOP)
    # root.mainloop()

class UserService(object):
    """
    註冊使用者
    """
    def __init__(self):
        pass

    @staticmethod
    def register(uid):
        if uid is None:
            return False
        url = "http://localhost:9999/register"
        response = requests.post(url, data={"uid": uid})
        if response.status_code == 200 and response.json()[`code`] == 1000:
            return True
        return False




class MemoHelper(object):
    """
    客戶端memo工具類
    """
    def __init__(self):
        self.url = "http://localhost:9999/{}"

    def post(self, uid, memoid, memo):
        posturl = self.url.format("add")
        payload = {
            "uid": uid,
            "securitycode": get_security_code(uid),
            "memoid": memoid,
            "memo": memo
        }
        response = requests.post(posturl, data=payload)
        return response.text
    def getmemo(self, uid, memoid):
        url = self.url.format("detail")
        payload = {
            "uid": uid,
            "securitycode": get_security_code(uid),
            "memoid": memoid
        }
        response = requests.post(url, data=payload)
        return response.text
    def getmemos(self, uid, reverse=True):
        url = self.url.format("lists")
        payload = {
            "uid": uid,
            "securitycode": get_security_code(uid),
            "reverse": reverse
        }
        response = requests.post(url, data=payload)
        return response.text
    def getrecent(self, uid):
        url = self.url.format("recent")
        payload = {
            "uid": uid,
            "securitycode": get_security_code(uid)
        }
        response = requests.post(url, data=payload)
        return response.text
    def updatememo(self, uid, memoid, memo):
        url = self.url.format("update")
        payload = {
            "uid": uid,
            "securitycode": get_security_code(uid),
            "memoid": memoid,
            "memo": memo
        }
        response = requests.post(url, data=payload)
        return response.text
    def deletememo(self, uid, memoid):
        url = self.url.format("delete")
        payload = {
            "uid": uid,
            "securitycode": get_security_code(uid),
            "memoid": memoid
        }
        response = requests.post(url, data=payload)
        return response.text


class Emitter(object):
    """
    檢測到到期任務,則彈出一個提醒框!
    """
    def __init__(self, uid):
        self.uid = str(uid)
        self.memohelper = MemoHelper()

    def emit_in(self, timestr, memo):
        timestamp = time_to_stamp(timestr)
        self.memohelper.post(self.uid, timestamp, memo)



    def emit_out(self):
        # 如果時間符合要求就emit出來, 找出距離時間最遠的memo。
        data = self.memohelper.getrecent(self.uid)
        data = json.loads(data)
        data = data[`msg`]
        data = json.loads(data)
        targettime = stamp_to_time(data[`memoid`])
        memo = data[`memo`]
        make_dialog(targettime, memo)


if __name__ == `__main__`:

    emitter = Emitter(201393260)
    emitter.emit_out()複製程式碼

總結

總體來說,這個小工具沒有達到我的預期效果,大致有如下幾點:

  • 掃描服務可以說沒怎麼做。
  • “安全”這裡簡單就是限制客戶端請求方式為POST,簡單的使用securitycode做了下判斷(如果被抓包分析的話,那也沒什麼用,比較好的做法就是模仿Flask使用PIN碼,來進一步提高安全性)。
  • 客戶端彈框效果不是很理想,而且對於釋出一個POST而言,還沒做對應的GUI支援,目前也就在命令列裡面使用類似:

    emitin xxxxxxx a:b:c:d
    大致的意思是: 記錄 在a天b小時c分鐘d秒 後要做的xxxxxxxx事項。

有興趣的可以在目前的程式碼基礎上進行優化,這裡權當拋磚引玉吧。

相關文章