🐛 自定义版本迭代 (#2)

 支持自定义字体。
 支持自定义帮助信息。
 支持自定义数据库地址。
 支持报错发送给 Owner.
 支持日志记录。
 支持被踢出群组后不再分析。
 支持更加顺滑的更新。
🐛 修复字体错误
🐛 将三条消息合并为一条。
️ 更好的排版。
⬇️ 降低默认主动触发频率限制
🔇 更加精确的过滤链接和命令
🌐 所依赖分词库仅支持中文分词,故不添加 i18n 。
This commit is contained in:
Xtao_dada 2021-06-20 19:07:26 +08:00 committed by GitHub
parent 78f20c465b
commit e23cf3cab5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 261 additions and 101 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea/
venv/
__pycache__/
config.py

29
config.gen.py Normal file
View File

@ -0,0 +1,29 @@
TOKEN = "token here"
# 频率限制次数每个群每小时内只能主动触发2次任务
LIMIT_COUNT = 2
# 私有模式,仅授权群组可用 False:关闭 True:打开
EXCLUSIVE_MODE = False
# 配置私有模式群组id列表不私有请忽略 例如:[-1001324252532, -100112415423]
EXCLUSIVE_LIST = []
# 主动触发命令仅管理员有效 False:否 True:是
RANK_COMMAND_MODE = True
# 中文字体路径
FRONT = 'fonts/ZhuZiAWan-2.ttc'
# Redis 配置
REDIS_CONFIG = {'host': '127.0.0.1', 'port': 6379, 'db': 1}
# 拥有者 id 配置
OWNER = 0
# 日志频道 id 0 为不启用
CHANNEL = 0
# 帮助信息
HELP = '<b>Group Word Cloud Bot</b>\n\n/start - 查看此帮助信息\n/ping - 我还活着吗?\n/rank - 手动生成词云(绒布球)\n\n' \
'此项目开源于https://git.io/JnrvH'

View File

@ -1,10 +0,0 @@
TOKEN = "1749418611:AAGOV2XB5mkMXqX-J_wtNu7KkrkhO_Xylmg"
# 频率限制次数每个群每小时内只能主动触发10次任务
LIMIT_COUNT = 10
# 私有模式,仅授权群组可用 0:关闭 1:打开
EXCLUSIVE_MODE = 0
# 主动触发命令仅管理员有效 0:否 1:是
RANK_COMMAND_MODE = 1

View File

@ -1,6 +1,8 @@
import redis import redis
from config import REDIS_CONFIG
pool = redis.ConnectionPool(host='127.0.0.1', port=6379, encoding='utf8', decode_responses=True) pool = redis.ConnectionPool(host=REDIS_CONFIG['host'], port=REDIS_CONFIG['port'], db=REDIS_CONFIG['db'],
encoding='utf8', decode_responses=True)
def get_connection(): def get_connection():

BIN
fonts/ZhuZiAWan-2.ttc Normal file

Binary file not shown.

59
func.py
View File

@ -3,7 +3,7 @@ import time
import connector import connector
import telegram import telegram
from telegram.ext import CommandHandler, MessageHandler, Filters from telegram.ext import CommandHandler, MessageHandler, Filters
from config import TOKEN, LIMIT_COUNT, EXCLUSIVE_MODE, RANK_COMMAND_MODE from config import TOKEN, LIMIT_COUNT, EXCLUSIVE_MODE, RANK_COMMAND_MODE, OWNER, EXCLUSIVE_LIST, CHANNEL, HELP
import schedule import schedule
from task import add_task from task import add_task
@ -11,17 +11,37 @@ bot = telegram.Bot(token=TOKEN)
def start(update, context): def start(update, context):
# 限制不为群组
chat_type = update.effective_chat.type
if chat_type == "supergroup":
return
try: try:
connector.get_connection().keys() connector.get_connection().keys()
print('进入 start 函数') print('进入 start 函数')
update.message.reply_text( update.message.reply_text(
'在呢!系统运行正常~', HELP,
parse_mode='HTML'
) )
except Exception as e: except Exception as e:
print(e) print(e)
print('进入start函数') print('进入start函数')
update.message.reply_text("系统故障Redis连接失败请检查") if update.effective_user.id == OWNER:
update.message.reply_text("错误信息:" + str(e)) update.message.reply_text(f"系统故障Redis连接失败错误信息\n{e}")
def ping(update, context):
# 限制不为群组
chat_type = update.effective_chat.type
if chat_type == "supergroup":
return
try:
connector.get_connection().keys()
print('进入 ping 函数')
update.message.reply_text(
'pong~',
)
except Exception as e:
print(e)
def rank(update, context): def rank(update, context):
@ -36,9 +56,9 @@ def rank(update, context):
username = update.effective_user.id username = update.effective_user.id
# 限制为群组 # 限制为群组
if chat_type != "supergroup": if chat_type != "supergroup":
update.message.reply_text("此命令只有在群组中有效") update.message.reply_text("此命令只有在群组中有效")
return return
if RANK_COMMAND_MODE == 1: if RANK_COMMAND_MODE:
try: try:
chat_member = bot.get_chat_member(chat_id, user_id) chat_member = bot.get_chat_member(chat_id, user_id)
status = chat_member["status"] status = chat_member["status"]
@ -46,7 +66,6 @@ def rank(update, context):
if status == "creator" or status == "administrator": if status == "creator" or status == "administrator":
print("用户权限正确") print("用户权限正确")
else: else:
update.message.reply_text("此命令仅对管理员开放")
return return
except Exception as e: except Exception as e:
print(e) print(e)
@ -71,6 +90,12 @@ def rank(update, context):
return return
add_task(chat_id) add_task(chat_id)
print("群组: {},用户: {}|{} 发起了主动触发请求".format(chat_id, username, user_id, )) print("群组: {},用户: {}|{} 发起了主动触发请求".format(chat_id, username, user_id, ))
if not CHANNEL == 0:
ctext = f'#WORDCLOUD #APPLY \n' \
f'群组 ID`{chat_id}`\n' \
f'用户 ID`{user_id}`\n' \
f'执行操作:`主动生成词云`'
bot.send_message(chat_id=CHANNEL, text=ctext, parse_mode="Markdown")
update.message.reply_text("统计数据将在分析完毕后发送到当前群组,请稍等~") update.message.reply_text("统计数据将在分析完毕后发送到当前群组,请稍等~")
except Exception as e: except Exception as e:
print("主动触发任务失败,请检查") print("主动触发任务失败,请检查")
@ -91,30 +116,19 @@ def chat_content_exec(update, context):
if len(text) > 80: if len(text) > 80:
return return
# 独享模式(仅授权群组可用) # 独享模式(仅授权群组可用)
if EXCLUSIVE_MODE == 1 and chat_id not in ["1231242141"]: if EXCLUSIVE_MODE and chat_id not in EXCLUSIVE_LIST:
print(chat_id + " 为未认证群组,取消入库") print(chat_id + " 为未认证群组,取消入库")
return return
try:
username = update.effective_user.username
except Exception as e:
username = update.effective_user.id
user = update.message.from_user user = update.message.from_user
firstname = str(user["first_name"]) firstname = str(user["first_name"])
lastname = str(user["last_name"]) name = firstname
name = ""
if firstname != "None":
name = firstname + " "
if lastname != "None":
name += lastname
if len(name) == 0:
name = username
print("\n---------------------------") print("\n---------------------------")
print("内容: " + text[:10]) print("内容: " + text[:10])
print("群组类型: " + str(chat_type)) print("群组类型: " + str(chat_type))
print("用户ID: " + str(user_id)) print("用户ID: " + str(user_id))
print("chat_id: " + str(chat_id)) print("chat_id: " + str(chat_id))
if "/" in text: if text.startswith('/') or '//' in text:
print("这是一条指令信息,跳过") print("这是一条指令或者链接信息,跳过")
return return
else: else:
if text[-1] not in ["", "", "", "", "", "!", "?", ",", ":", "."]: if text[-1] not in ["", "", "", "", "", "!", "?", ",", ":", "."]:
@ -136,5 +150,6 @@ def check_schedule():
start_handler = CommandHandler('start', start) start_handler = CommandHandler('start', start)
ping_handler = CommandHandler('ping', ping)
rank_handler = CommandHandler('rank', rank) rank_handler = CommandHandler('rank', rank)
chat_content_handler = MessageHandler(Filters.text, chat_content_exec) chat_content_handler = MessageHandler(Filters.text, chat_content_exec)

126
images/README.md Normal file
View File

@ -0,0 +1,126 @@
## 当前版本
v2.3
## 更新维护日志
[更新维护日志](https://github.com/devourbots/word_cloud_bot/wiki/%E6%9B%B4%E6%96%B0%E7%BB%B4%E6%8A%A4%E6%97%A5%E5%BF%97)
## 有问题请加群组反馈
Telegram 交流反馈群组 [点击加入](https://t.me/devourbots)
## 演示
![xq9iR.png](https://s3.jpg.cm/2021/05/05/xq9iR.png)
## 配置要求
内存1G以上
## 安装方法
### 使用 Docker 安装
Docker官方安装地址[点击访问](https://docs.docker.com/engine/install/)
```angular2html
cd /root
# 拉取Redis镜像
docker pull redis
# 创建 entrypoint.sh 入口文件
echo '#! /bin/sh \
cd /root/word_cloud_bot && python3 main.py >> output 2>&1 &
tail -f /dev/null' > /root/entrypoint.sh
# 创建 Dockerfile
wget -O /root/Dockerfile https://github.com/devourbots/word_cloud_bot/raw/master/Dockerfile
# 使用命令查看所有时区
timedatectl list-timezones
找到您所在的时区,例如:
上海 Asia/Shanghai
纽约 America/New_York
# 编辑Dockerfile
vi /root/Dockerfile
# 在第7行修改服务器所属时区原文件为
RUN ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
修改为纽约当地时,修改后:
RUN ln -s /usr/share/zoneinfo/America/New_York /etc/localtime
# 在第10行修改你的机器人TOKEN
修改后:
RUN sed -i '1c TOKEN = "1749418611:AAGcpouQ4EWSDITLQXFozHjMgT_-MsVSmDM"' /root/word_cloud_bot/config.py
# 根据 Dockerfile 创建镜像
docker build . -t world_cloud_bot:latest
# 运行 Redis 镜像,此步在前
docker run -d -p 6379:6379 redis:latest
# 注意!!!
请关闭服务器 6379 端口的外网访问权限如果您的主机提供商提供了安全组策略阿里云、腾讯云、AWS等等可以在控制台关闭6379端口。
如果您的主机商不支持自定义安全组,请根据您的发行版系统自行搜索防火墙关闭端口的方式,检测方式在下方。
不要抱有侥幸心理!不要抱有侥幸心理!不要抱有侥幸心理!
# 运行 机器人,此步在后
docker run -d --net=host world_cloud_bot:latest
```
[端口检测工具](http://tool.chinaz.com/port/), 请确保 6379 是关闭状态
![xlu8H.png](https://s3.jpg.cm/2021/05/06/xlu8H.png)
## 使用方法
使用 `/start` 指令测试机器人与 Redis 数据库的连通情况
使用 `/rank` 指令主动触发词云任务,在 config.py 里可以设置每个群组每小时主动触发次数的限制
将机器人拉入群组设置为管理员受机器人API所限只有授予管理员权限后机器人才能接收到所有用户的普通聊天文本此机器人不需要其他权限您可以将所有权限关闭
所有聊天内容每天定时清理,仅用于本地分词,无其他任何用途
![xqyvt.png](https://s3.jpg.cm/2021/05/05/xqyvt.png)
### 将机器人设置为仅自己群组可用
如何编辑 Docker 容器中的文件请自行 Google
如果您不想让别人使用你的机器人,那么可以将 `config.py` 文件中的 `EXCLUSIVE_MODE = 0`改为 `EXCLUSIVE_MODE = 1`
![DGbSy.png](https://s3.jpg.cm/2021/05/09/DGbSy.png)
编辑 `/root/word_cloud_bot/func.py`,在 94 行左右,将自己的 群组ID 加入到列表中。
这里的`EXCLUSIVE_MODE = 1`不要改动,注意区分!
例如我两个的群组ID分别为-127892174935、-471892571924
那么修改后为:
```angular2html
if EXCLUSIVE_MODE == 1 and chat_id not in ["-127892174935", "-471892571924"]:
print(chat_id + " 为未认证群组,取消入库")
return
```
![DGHR5.png](https://s3.jpg.cm/2021/05/09/DGHR5.png)
### 设置 /rank 指令对普通用户开放
编辑 `/root/word_cloud_bot/config.py``RANK_COMMAND_MODE = 1` 改为 `RANK_COMMAND_MODE = 0`
![DGJuC.png](https://s3.jpg.cm/2021/05/09/DGJuC.png)
### 信息推送密度
![xW3jh.png](https://s3.jpg.cm/2021/05/05/xW3jh.png)
默认分别会在当地时间 11:00、18:00、23:30 推送三次数据统计报告,并会在 23:59 清空当日统计数据,
如需更密集的数据推送,可以编辑 /root/word_cloud_bot/main.py ,按照示例格式自行增加,相关的 docker 技术操作不再赘述

BIN
images/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 KiB

View File

@ -1,11 +1,12 @@
from telegram.ext import Updater from telegram.ext import Updater
from config import TOKEN from config import TOKEN
from func import start_handler, chat_content_handler, check_schedule, rank_handler from func import start_handler, ping_handler, chat_content_handler, check_schedule, rank_handler
import schedule import schedule
from task import schedule_task, flush_redis, do_task from task import schedule_task, flush_redis, do_task
import threading import threading
schedule.every().day.at('11:00').do(schedule_task) # 开始定时任务 - 群组分析
schedule.every().day.at('12:00').do(schedule_task)
schedule.every().day.at('18:00').do(schedule_task) schedule.every().day.at('18:00').do(schedule_task)
schedule.every().day.at('23:30').do(schedule_task) schedule.every().day.at('23:30').do(schedule_task)
schedule.every().day.at('23:59').do(flush_redis) schedule.every().day.at('23:59').do(flush_redis)
@ -22,6 +23,7 @@ updater = Updater(token=TOKEN, use_context=True)
dispatcher = updater.dispatcher dispatcher = updater.dispatcher
dispatcher.add_handler(start_handler) dispatcher.add_handler(start_handler)
dispatcher.add_handler(ping_handler)
dispatcher.add_handler(rank_handler) dispatcher.add_handler(rank_handler)
dispatcher.add_handler(chat_content_handler) dispatcher.add_handler(chat_content_handler)

122
task.py
View File

@ -8,7 +8,7 @@ import telegram
import time import time
import os import os
import connector import connector
from config import TOKEN from config import TOKEN, FRONT, CHANNEL
bot = telegram.Bot(token=TOKEN) bot = telegram.Bot(token=TOKEN)
@ -24,10 +24,10 @@ def schedule_task():
if "chat_content" in i: if "chat_content" in i:
group_list.append(i[:i.find("_")]) group_list.append(i[:i.find("_")])
# print(group_list) # print(group_list)
print("运行定时任务,任务队列中添加任务,任务数量:{}".format(len(group_list))) print("运行定时任务,任务队列中添加任务,任务数量:{}".format(len(group_list)))
for group in group_list: for group in group_list:
try: try:
# 任务队列中添加任务 # 任务队列中添加任务
task_queue.put(group) task_queue.put(group)
except Exception as e: except Exception as e:
print("群组:{} | 词云数据分析生成失败,请查看报错信息".format(group)) print("群组:{} | 词云数据分析生成失败,请查看报错信息".format(group))
@ -49,9 +49,20 @@ def do_task():
stop_time = float(time.time()) stop_time = float(time.time())
print("当前群组处理耗时:" + str(stop_time - start_time)) print("当前群组处理耗时:" + str(stop_time - start_time))
print("---------------------------") print("---------------------------")
ctext = f'#WORDCLOUD \n' \
f'群组 ID`{group}`\n' \
f'执行操作:`生成词云`\n' \
f'结果:`成功`\n' \
f'处理耗时:`{str(stop_time - start_time)[:5]}`'
except Exception as e: except Exception as e:
print("群组: {} | 处理失败,可能是机器人已经被移出群组,请检查报错!".format(group)) print("群组: {} | 处理失败,可能是机器人已经被移出群组,请检查报错!".format(group))
print(e) print(e)
ctext = f'#WORDCLOUD #SCHEDULE \n' \
f'群组 ID`{group}`\n' \
f'执行操作:`生成词云`\n' \
f'结果:`失败`\n'
if not CHANNEL == 0:
bot.send_message(chat_id=CHANNEL, text=ctext, parse_mode="Markdown")
time.sleep(1) time.sleep(1)
@ -61,12 +72,12 @@ def add_task(group):
# 核心函数,分词统计 # 核心函数,分词统计
def generate(group): def generate(group):
mk = imageio.imread("/root/word_cloud_bot/circle.png") mk = imageio.imread("circle.png")
# 构建并配置词云对象w注意要加scale参数提高清晰度 # 构建并配置词云对象w注意要加scale参数提高清晰度
w = wordcloud.WordCloud(width=800, w = wordcloud.WordCloud(width=800,
height=800, height=800,
background_color='white', background_color='white',
font_path='/root/word_cloud_bot/font.ttf', font_path=FRONT,
mask=mk, mask=mk,
scale=5) scale=5)
r = connector.get_connection() r = connector.get_connection()
@ -76,15 +87,7 @@ def generate(group):
chat_content = r.get("{}_chat_content".format(group)) chat_content = r.get("{}_chat_content".format(group))
if chat_content is None: if chat_content is None:
print("数据库中不存在此群组数据") print("数据库中不存在此群组 {} 数据".format(group))
try:
time.sleep(1)
bot.send_message(
chat_id=group,
text="数据库中不存在群组数据,请检查是否授予机器人管理员权限,并通过聊天添加数据量,嗨起来吧~\n"
)
except Exception as e:
print("群组: {} | 机器人发送信息失败".format(group))
return return
word_list = [] word_list = []
words = pseg.cut(chat_content, use_paddle=True) # paddle模式 words = pseg.cut(chat_content, use_paddle=True) # paddle模式
@ -107,8 +110,11 @@ def generate(group):
user_message_amount = r.hgetall("{}_user_message_amount".format(group)) user_message_amount = r.hgetall("{}_user_message_amount".format(group))
user_message_amount = sorted(user_message_amount.items(), key=lambda kv: (int(kv[1])), reverse=True) user_message_amount = sorted(user_message_amount.items(), key=lambda kv: (int(kv[1])), reverse=True)
if len(word_list) > 0: # 截至时间
date = time.strftime("%Y年%m月%d", time.localtime()) + '' + time.strftime("%H:%M", time.localtime())
text = f'📅 截至 {date}\n'
# 分析高频词 # 分析高频词
if len(word_list) > 0:
word_amount = {} word_amount = {}
# print(word_amount) # print(word_amount)
for word in word_list: for word in word_list:
@ -128,77 +134,63 @@ def generate(group):
hot_word_string = "" hot_word_string = ""
# 默认展示前5位少于5个则全部展示 # 默认展示前5位少于5个则全部展示
for i in range(min(5, len(word_amount))): for i in range(min(5, len(word_amount))):
hot_word_string += "\t\t\t\t\t\t\t\t" + "`" + str(word_amount[i][0]) + "`" + ": " + str( hot_word_string += "\t\t\t\t\t\t\t\t" + "👥 `" + str(word_amount[i][0]) + "`" + "" + str(
word_amount[i][1]) + "\n" word_amount[i][1]) + "\n"
# print(hot_word_string) # print(hot_word_string)
bot.send_message( text += f"🗣️ 本群 {user_amount} 位成员共产生 {total_message_amount} 条发言\n" \
chat_id=group, f"🤹‍ 大家今天讨论最多的是:\n\n{hot_word_string}\n"
text="🎤 今日话题榜 🎤\n"
"📅 {}\n"
"⏱ 截至今天{}\n"
"🗣️ 本群{}位朋友共产生{}条发言\n"
"🤹‍ 大家今天讨论最多的是:\n\n"
"{}\n"
"看下有没有你感兴趣的话题? 👏".format(
time.strftime("%Y年%m月%d", time.localtime()),
time.strftime("%H:%M", time.localtime()),
user_amount,
total_message_amount,
hot_word_string),
parse_mode="Markdown"
)
else: else:
bot.send_message( text += '无法分析出当前群组的热词列表,可能是数据量过小,嗨起来吧~\n'
chat_id=group, else:
text="当前聊天数据量过小,嗨起来吧~" text += '无法分析出当前群组的热词列表,可能是数据量过小,嗨起来吧~\n'
)
# 分析活跃用户
if len(user_message_amount) > 0: if len(user_message_amount) > 0:
# print("排序后的用户:" + str(user_message_amount)) # print("排序后的用户:" + str(user_message_amount))
top_5_user = "" top_5_user = ""
# 默认展示前5位少于5个则全部展示 # 默认展示前5位少于5个则全部展示
for i in range(min(5, len(user_message_amount))): for i in range(min(5, len(user_message_amount))):
dis_name = str(user_message_amount[i][0]) dis_name = str(user_message_amount[i][0])
top_5_user += "\t\t\t\t\t\t\t\t" + "🎖`" + dis_name[:min(8, len(dis_name))] + "`" + " 贡献: " + str( top_5_user += "\t\t\t\t\t\t\t\t" + "🎖 `" + dis_name[:min(10, len(dis_name))] + "`" + " 贡献:" + str(
user_message_amount[i][1]) + "\n" user_message_amount[i][1]) + "\n"
# print(top_5_user) # print(top_5_user)
bot.send_message( text += f"🏵 今日活跃用户排行榜:\n\n{top_5_user}"
chat_id=group,
text="🏵 今日活跃用户排行榜 🏵\n"
"📅 {}\n"
"⏱ 截至今天{}\n\n"
"{}\n"
"感谢这些朋友今天的分享! 👏 \n"
"遇到问题,向他们请教说不定有惊喜😃".format(
time.strftime("%Y年%m月%d", time.localtime()),
time.strftime("%H:%M", time.localtime()),
top_5_user),
parse_mode="Markdown"
)
else: else:
bot.send_message( text = '无法分析出当前群组的活跃用户列表,可能是数据量过小,嗨起来吧~'
chat_id=group,
text="当前聊天数据量过小,嗨起来吧~"
)
# 开始创建词云
img_path = 'images/default.png'
try: try:
string = " ".join(word_list) string = " ".join(word_list)
# 将string变量传入w的generate()方法,给词云输入文字 # 将string变量传入w的generate()方法,给词云输入文字
w.generate(string) w.generate(string)
# 将词云图片导出到当前文件夹 # 将词云图片导出到 images 文件夹
w.to_file('{}_chat_word_cloud.png'.format(group)) w.to_file('images/{}_chat_word_cloud.png'.format(group))
bot.send_photo( img_path = 'images/{}_chat_word_cloud.png'.format(group)
chat_id=group,
photo=open("{}_chat_word_cloud.png".format(group), "rb")
)
os.remove("{}_chat_word_cloud.png".format(group))
except Exception as e: except Exception as e:
print(e) print(e)
print("词云图片生成失败") print("词云图片生成失败")
# bot.send_message(
# chat_id=group, # 发送结果
# text="当前聊天数据量过小,嗨起来吧~" try:
# ) bot.send_photo(
chat_id=group,
photo=open(img_path, "rb"),
caption=text,
parse_mode='Markdown',
disable_notification=True
)
except Exception as e:
print(e)
r.delete('{}_chat_content'.format(group))
print("发送结果失败")
# 删除图片
try:
os.remove("images/{}_chat_word_cloud.png".format(group))
except Exception as e:
print(e)
print("删除图片失败")
def flush_redis(): def flush_redis():