From 4a6d2f82e0b6a7d5d98e5976f039e4210011bf3e Mon Sep 17 00:00:00 2001 From: Ching Date: Wed, 5 Jan 2022 11:28:02 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(scripts=20folder):=20[A]=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=98=9F=E5=98=9F=E6=9C=BA=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [A]增加嘟嘟机脚本 Signed-off-by: Ching --- scripts/dodo.py | 185 ++++++++++++++++++++++++++++++++++++ toot/__init__.py | 0 toot/admin.py | 3 + toot/apps.py | 6 ++ toot/migrations/__init__.py | 0 toot/models.py | 1 + toot/tests.py | 3 + toot/views.py | 3 + utils/depoly_notify.py | 25 +++++ 9 files changed, 226 insertions(+) create mode 100644 scripts/dodo.py create mode 100644 toot/__init__.py create mode 100644 toot/admin.py create mode 100644 toot/apps.py create mode 100644 toot/migrations/__init__.py create mode 100644 toot/models.py create mode 100644 toot/tests.py create mode 100644 toot/views.py create mode 100755 utils/depoly_notify.py diff --git a/scripts/dodo.py b/scripts/dodo.py new file mode 100644 index 0000000..3af5dc8 --- /dev/null +++ b/scripts/dodo.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +# --coding:utf-8-- + +from http.server import BaseHTTPRequestHandler, HTTPServer +import json +from urllib import request +import hashlib +import base64 +from Crypto.Cipher import AES + +# from utils import get_tenant_access_token, isreciept +# from Function import * +# from Private import APP_VERIFICATION_TOKEN + +APP_VERIFICATION_TOKEN = 'uKQQiOVMYg2cTgrjkyBmodrHTUaCXzG3' +APP_ID = 'cli_a115fe8b83f9100c' +APP_SECRET = 'yuSQenId0VfvwdZ3qL9wMd8FpCMEUL0u' +ENCRYPT_KEY = '4XfjcA5xou3pztBD4g5V7dgHtr0BBYDE' +EVENT_TYPE = ['im.message.receive_v1'] + + +class AESCipher(object): + def __init__(self, key): + self.bs = AES.block_size + self.key=hashlib.sha256(AESCipher.str_to_bytes(key)).digest() + @staticmethod + def str_to_bytes(data): + u_type = type(b"".decode('utf8')) + if isinstance(data, u_type): + return data.encode('utf8') + return data + @staticmethod + def _unpad(s): + return s[:-ord(s[len(s) - 1:])] + def decrypt(self, enc): + iv = enc[:AES.block_size] + cipher = AES.new(self.key, AES.MODE_CBC, iv) + return self._unpad(cipher.decrypt(enc[AES.block_size:])) + def decrypt_string(self, enc): + enc = base64.b64decode(enc) + return self.decrypt(enc).decode('utf8') + +def get_tenant_access_token(): # 获取token + + url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/" + headers = { + "Content-Type": "application/json" + } + req_body = { + "app_id": APP_ID, + "app_secret": APP_SECRET + } + + data = bytes(json.dumps(req_body), encoding='utf8') + req = request.Request(url=url, data=data, headers=headers, method='POST') + try: + response = request.urlopen(req) + except Exception as e: + print(e.read().decode()) + return "" + + rsp_body = response.read().decode('utf-8') + rsp_dict = json.loads(rsp_body) + code = rsp_dict.get("code", -1) + if code != 0: + print("get tenant_access_token error, code =", code) + return "" + return rsp_dict.get("tenant_access_token", "") + + +class RequestHandler(BaseHTTPRequestHandler): + def do_POST(self): + # 解析请求 body + req_body = self.rfile.read(int(self.headers['content-length'])) + obj = json.loads(req_body.decode("utf-8")) + cipher = AESCipher(ENCRYPT_KEY) + obj = json.loads(cipher.decrypt_string(obj['encrypt'])) + print(req_body) + print(obj) + + # 校验 verification token 是否匹配,token 不匹配说明该回调并非来自开发平台 + token = obj.get("token", "") + if not token: + token = obj.get('header', {}).get('token', '') + if token != APP_VERIFICATION_TOKEN: + print("verification token not match, token =", token) + self.response("") + return + + # 根据 type 处理不同类型事件 + type = obj.get("type", "") + if not type: + type = obj.get('header', {}).get('event_type') + if "url_verification" == type: # 验证请求 URL 是否有效 + self.handle_request_url_verify(obj) + elif type in EVENT_TYPE: # 事件回调 + # 获取事件内容和类型,并进行相应处理,此处只关注给机器人推送的消息事件 + event = obj.get("event") + if event.get("message"): + self.handle_message(event) + return + return + + def handle_request_url_verify(self, post_obj): + # 原样返回 challenge 字段内容 + challenge = post_obj.get("challenge", "") + rsp = {'challenge': challenge} + self.response(json.dumps(rsp)) + return + + def handle_message(self, event): + # 此处只处理 text 类型消息,其他类型消息忽略 + msg = event.get('message', {}) + msg_type = msg.get("message_type", "") + if msg_type == "text": + # 调用发消息 API 之前,先要获取 API 调用凭证:tenant_access_token + access_token = get_tenant_access_token() + if access_token == "": + self.response("") + return + + # 机器人回复收到的消息 + text = json.loads(msg.get('content')).get('text') + if msg.get('chat_type') == 'group' and msg.get('mentions'): + open_id = {"open_chat_id": msg.get("chat_id")} + for mention in msg.get('mentions'): + text = text.replace(mention['key'], '') + text = text.lstrip() + else: + open_id = {"open_id": event.get("sender", {}).get( + 'sender_id', {}).get('open_id')} + self.msg_compoment(access_token, open_id, text) + self.response("") + return + elif msg_type == "image": + self.response("") + return + + def response(self, body): + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(body.encode()) + + def send_message(self, token, open_id, text): + url = "https://open.feishu.cn/open-apis/message/v4/send/" + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer " + token + } + req_body = { + "msg_type": "text", + "content": { + "text": text + } + } + req_body = dict(req_body, **open_id) # 根据open_id判断返回域 + + data = bytes(json.dumps(req_body), encoding='utf8') + req = request.Request(url=url, data=data, headers=headers, method='POST') + try: + response = request.urlopen(req) + except Exception as e: + print(e.read().decode()) + return + + rsp_body = response.read().decode('utf-8') + rsp_dict = json.loads(rsp_body) + code = rsp_dict.get("code", -1) + if code != 0: + print("send message error, code = ", code, ", msg =", rsp_dict.get("msg", "")) + + def msg_compoment(self, token, open_id, text): + self.send_message(token, open_id, text) + +def run(): + port = 5000 + server_address = ('', port) + httpd = HTTPServer(server_address, RequestHandler) + print("start.....") + httpd.serve_forever() + + +if __name__ == '__main__': + run() diff --git a/toot/__init__.py b/toot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toot/admin.py b/toot/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/toot/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/toot/apps.py b/toot/apps.py new file mode 100644 index 0000000..426e9bf --- /dev/null +++ b/toot/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TootConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'toot' diff --git a/toot/migrations/__init__.py b/toot/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toot/models.py b/toot/models.py new file mode 100644 index 0000000..137941f --- /dev/null +++ b/toot/models.py @@ -0,0 +1 @@ +from django.db import models diff --git a/toot/tests.py b/toot/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/toot/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/toot/views.py b/toot/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/toot/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/utils/depoly_notify.py b/utils/depoly_notify.py new file mode 100755 index 0000000..51ec521 --- /dev/null +++ b/utils/depoly_notify.py @@ -0,0 +1,25 @@ +# -*- coding: UTF-8 -*- +import sys +import os +from imp import reload +sys.path.insert(0, os.path.abspath('..')) +sys.path.append('/Users/ching/develop/dsite') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "../dsite.settings") +reload(sys) + +import utils.lark +import git +import ipdb + +if __name__ == '__main__': + # def notify(): + repo = git.Repo(search_parent_directories=True) + commit = repo.head.commit + rev, branch = commit.name_rev.split(' ') + msg = 'rev: %s\n\nauther: %s\n\nbranch: %s\n\nmessage: %s' % ( + rev, + commit.author.name, + branch, + commit.summary + ) + print(utils.lark.request({'text': msg})) From a8fff9c533fd625bd46a2cc2f62b69b2c0ed35ff Mon Sep 17 00:00:00 2001 From: Ching Date: Wed, 5 Jan 2022 13:44:29 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(dodo.py):=20[M]=20=E5=98=9F=E5=98=9F?= =?UTF-8?q?=E6=9C=BA=E5=A2=9E=E5=8A=A0=E5=8F=91=E9=80=81=E5=88=B0nofan=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [M] 嘟嘟机增加发送到nofan 功能 Signed-off-by: Ching --- scripts/dodo.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/dodo.py b/scripts/dodo.py index 3af5dc8..53ae807 100644 --- a/scripts/dodo.py +++ b/scripts/dodo.py @@ -7,6 +7,8 @@ from urllib import request import hashlib import base64 from Crypto.Cipher import AES +from mastodon import Mastodon +import logging # from utils import get_tenant_access_token, isreciept # from Function import * @@ -18,6 +20,13 @@ APP_SECRET = 'yuSQenId0VfvwdZ3qL9wMd8FpCMEUL0u' ENCRYPT_KEY = '4XfjcA5xou3pztBD4g5V7dgHtr0BBYDE' EVENT_TYPE = ['im.message.receive_v1'] +logging.basicConfig(filename='/root/develop/log/dodo.log', level=logging.INFO) +logger = logging.getLogger('/root/develop/log/dodo.log') + +mastodon = Mastodon( + access_token = 'Ug_bUMWCk3RLamOnqYIytmeB0nO6aNfjdmf06mAj2bE', + api_base_url = 'https://nofan.xyz' +) class AESCipher(object): def __init__(self, key): @@ -129,8 +138,13 @@ class RequestHandler(BaseHTTPRequestHandler): else: open_id = {"open_id": event.get("sender", {}).get( 'sender_id', {}).get('open_id')} - self.msg_compoment(access_token, open_id, text) self.response("") + try: + toot_resp = mastodon.status_post(text) + if toot_resp.get('id'): + self.msg_compoment(access_token, open_id, '📟 dodo 📟') + except: + pass return elif msg_type == "image": self.response("") From 78ef05b68e8ddbd685c6d04956f096791d8a709d Mon Sep 17 00:00:00 2001 From: Ching Date: Fri, 7 Jan 2022 11:22:20 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(dodo.py):=20[M]=20add=20logger;=20?= =?UTF-8?q?=E7=BC=93=E5=AD=98event=5Fid=E9=81=BF=E5=85=8D=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=8F=91=E9=80=81=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [M] add logger; 缓存event_id避免重复发送消息 Signed-off-by: Ching --- develop_requirements.txt | 1 + scripts/dodo.py | 83 +++++++++++++++++++++++++++++++--------- 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/develop_requirements.txt b/develop_requirements.txt index 9b0502c..89452a9 100644 --- a/develop_requirements.txt +++ b/develop_requirements.txt @@ -30,3 +30,4 @@ ua-parser==0.10.0 user-agents==2.2.0 wcwidth==0.2.5 zipp==3.5.0 +redis==4.1.0 diff --git a/scripts/dodo.py b/scripts/dodo.py index 53ae807..54a0f36 100644 --- a/scripts/dodo.py +++ b/scripts/dodo.py @@ -9,16 +9,16 @@ import base64 from Crypto.Cipher import AES from mastodon import Mastodon import logging - -# from utils import get_tenant_access_token, isreciept -# from Function import * -# from Private import APP_VERIFICATION_TOKEN +import redis +import requests +import time APP_VERIFICATION_TOKEN = 'uKQQiOVMYg2cTgrjkyBmodrHTUaCXzG3' APP_ID = 'cli_a115fe8b83f9100c' APP_SECRET = 'yuSQenId0VfvwdZ3qL9wMd8FpCMEUL0u' ENCRYPT_KEY = '4XfjcA5xou3pztBD4g5V7dgHtr0BBYDE' EVENT_TYPE = ['im.message.receive_v1'] +ADD_GROUP_NAME = True logging.basicConfig(filename='/root/develop/log/dodo.log', level=logging.INFO) logger = logging.getLogger('/root/develop/log/dodo.log') @@ -28,6 +28,9 @@ mastodon = Mastodon( api_base_url = 'https://nofan.xyz' ) +pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True) +redis_cli = redis.Redis(host='localhost', port=6379, decode_responses=True) + class AESCipher(object): def __init__(self, key): self.bs = AES.block_size @@ -50,6 +53,9 @@ class AESCipher(object): return self.decrypt(enc).decode('utf8') def get_tenant_access_token(): # 获取token + token = redis_cli.get('tenant_access_token_%s' % APP_ID) + if token: + return token url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/" headers = { @@ -65,16 +71,42 @@ def get_tenant_access_token(): # 获取token try: response = request.urlopen(req) except Exception as e: - print(e.read().decode()) + logger.error('get tenant token error: %s', e.read().decode()) return "" rsp_body = response.read().decode('utf-8') rsp_dict = json.loads(rsp_body) code = rsp_dict.get("code", -1) if code != 0: - print("get tenant_access_token error, code =", code) + logger.error("get tenant_access_token error, code =%s", code) return "" - return rsp_dict.get("tenant_access_token", "") + token = redis_cli.set('tenant_access_token_%s' % APP_ID, + rsp_dict.get("tenant_access_token", ""), + ex=60*30) + + return token + +def get_group_name(chat_id): + group_name = redis_cli.get('group_name_%s' % chat_id) + if not group_name: + url = "https://open.feishu.cn/open-apis/im/v1/chats/" + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer " + get_tenant_access_token() + } + try: + resp = requests.get(url+chat_id, headers=headers) + resp_data = resp.json() + code = resp_data.get("code", -1) + if code == 0: + group_name = resp_data.get('data', {}).get('name') + redis_cli.set('group_name_%s' % chat_id, + group_name, + ex=60*60*24) + except: + # todo: log + return + return group_name class RequestHandler(BaseHTTPRequestHandler): @@ -84,15 +116,14 @@ class RequestHandler(BaseHTTPRequestHandler): obj = json.loads(req_body.decode("utf-8")) cipher = AESCipher(ENCRYPT_KEY) obj = json.loads(cipher.decrypt_string(obj['encrypt'])) - print(req_body) - print(obj) + logger.info('lark request body: %s', obj) # 校验 verification token 是否匹配,token 不匹配说明该回调并非来自开发平台 token = obj.get("token", "") if not token: token = obj.get('header', {}).get('token', '') if token != APP_VERIFICATION_TOKEN: - print("verification token not match, token =", token) + logger.error("verification token not match, token =%s", token) self.response("") return @@ -104,10 +135,15 @@ class RequestHandler(BaseHTTPRequestHandler): self.handle_request_url_verify(obj) elif type in EVENT_TYPE: # 事件回调 # 获取事件内容和类型,并进行相应处理,此处只关注给机器人推送的消息事件 + event_id = obj.get('header', {}).get('event_id', '') + # 重复收到的事件不处理 + if event_id and redis_cli.get(event_id): + self.response("") + return event = obj.get("event") if event.get("message"): - self.handle_message(event) - return + self.handle_message(event, event_id) + return return def handle_request_url_verify(self, post_obj): @@ -117,7 +153,7 @@ class RequestHandler(BaseHTTPRequestHandler): self.response(json.dumps(rsp)) return - def handle_message(self, event): + def handle_message(self, event, event_id=None): # 此处只处理 text 类型消息,其他类型消息忽略 msg = event.get('message', {}) msg_type = msg.get("message_type", "") @@ -135,6 +171,9 @@ class RequestHandler(BaseHTTPRequestHandler): for mention in msg.get('mentions'): text = text.replace(mention['key'], '') text = text.lstrip() + if ADD_GROUP_NAME: + group_name = get_group_name(msg.get("chat_id")) + text = '%s #%s' % (text, group_name) else: open_id = {"open_id": event.get("sender", {}).get( 'sender_id', {}).get('open_id')} @@ -143,8 +182,14 @@ class RequestHandler(BaseHTTPRequestHandler): toot_resp = mastodon.status_post(text) if toot_resp.get('id'): self.msg_compoment(access_token, open_id, '📟 dodo 📟') - except: - pass + redis_cli.set(event_id, int(time.time()), ex=60*60*7) + else: + self.msg_compoment(access_token, open_id, """⚠️ didi ⚠️ + %s + """ % json.loads(toot_resp)) + except Exception as exc: + logger.error('send toot error: %s', str(exc)) + return elif msg_type == "image": self.response("") @@ -175,14 +220,16 @@ class RequestHandler(BaseHTTPRequestHandler): try: response = request.urlopen(req) except Exception as e: - print(e.read().decode()) + logger.error('send message error: %s', e.read().decode()) return rsp_body = response.read().decode('utf-8') rsp_dict = json.loads(rsp_body) code = rsp_dict.get("code", -1) if code != 0: - print("send message error, code = ", code, ", msg =", rsp_dict.get("msg", "")) + logger.error("send message error, code = %s, msg =%s", + code, + rsp_dict.get("msg", "")) def msg_compoment(self, token, open_id, text): self.send_message(token, open_id, text) @@ -191,7 +238,7 @@ def run(): port = 5000 server_address = ('', port) httpd = HTTPServer(server_address, RequestHandler) - print("start.....") + logger.info("start...") httpd.serve_forever() From 047bd00545253406ab324dc8be083770f43b643d Mon Sep 17 00:00:00 2001 From: Ching Date: Fri, 7 Jan 2022 11:59:11 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(dodo.py):=20[M]=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=B8=8A=E4=B8=80=E6=9D=A1=E5=92=8C=E5=88=A0=E9=99=A4=E4=B8=8A?= =?UTF-8?q?=E4=B8=80=E6=9D=A1=E5=98=9F=E5=98=9F=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [M] 增加上一条和删除上一条嘟嘟的功能 Signed-off-by: Ching --- scripts/dodo.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/dodo.py b/scripts/dodo.py index 54a0f36..9b534dd 100644 --- a/scripts/dodo.py +++ b/scripts/dodo.py @@ -19,6 +19,7 @@ APP_SECRET = 'yuSQenId0VfvwdZ3qL9wMd8FpCMEUL0u' ENCRYPT_KEY = '4XfjcA5xou3pztBD4g5V7dgHtr0BBYDE' EVENT_TYPE = ['im.message.receive_v1'] ADD_GROUP_NAME = True +KEDAI_ID = '107263380636355825' logging.basicConfig(filename='/root/develop/log/dodo.log', level=logging.INFO) logger = logging.getLogger('/root/develop/log/dodo.log') @@ -166,11 +167,13 @@ class RequestHandler(BaseHTTPRequestHandler): # 机器人回复收到的消息 text = json.loads(msg.get('content')).get('text') + orig_text = text if msg.get('chat_type') == 'group' and msg.get('mentions'): open_id = {"open_chat_id": msg.get("chat_id")} for mention in msg.get('mentions'): text = text.replace(mention['key'], '') text = text.lstrip() + orig_text = text if ADD_GROUP_NAME: group_name = get_group_name(msg.get("chat_id")) text = '%s #%s' % (text, group_name) @@ -178,6 +181,24 @@ class RequestHandler(BaseHTTPRequestHandler): open_id = {"open_id": event.get("sender", {}).get( 'sender_id', {}).get('open_id')} self.response("") + if orig_text == '/last': + try: + statuses = mastodon.account_statuses(KEDAI_ID, limit=1) + self.msg_compoment(access_token, open_id, + statuses[0]['content']) + except Exception as exc: + logger.error('operation error: %s', str(exc)) + elif orig_text == '/del': + try: + statuses = mastodon.account_statuses(KEDAI_ID, limit=1) + Mastodon.status_delete(statuses[0]['id']) + self.msg_compoment(access_token, open_id, + '已删除: ' + statuses[0]['content']) + except Exception as exc: + logger.error('operation error: %s', str(exc)) + + + try: toot_resp = mastodon.status_post(text) if toot_resp.get('id'):