Skip to content

Commit

Permalink
feat(user): 添加用户插件 (#381)
Browse files Browse the repository at this point in the history
提供统一的用户,支持绑定各平台用户。
  • Loading branch information
he0119 authored Sep 14, 2023
1 parent 6f2ef1c commit 405fad3
Show file tree
Hide file tree
Showing 17 changed files with 1,372 additions and 169 deletions.
27 changes: 18 additions & 9 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,29 @@ SUPERUSERS=[]
COMMAND_START=["/", "/"]
COMMAND_SEP=[".","。"]

# 机器人实现配置 -----------------------------

# OneBot 配置
# https://onebot.adapters.nonebot.dev/docs/guide/configuration
ONEBOT_ACCESS_TOKEN
ONEBOT_V12_ACCESS_TOKEN
ONEBOT_V12_USE_MSGPACK=true

# Red
# https://github.com/nonebot/adapter-red
RED_BOTS=[]

# QQ 频道
# https://github.com/nonebot/adapter-qqguild
QQGUILD_BOTS=[]
QQGUILD_IS_SANDBOX=false

# 开黑啦
# https://github.com/Tian-que/nonebot-adapter-kaiheila/blob/master/MANUAL.md
KAIHEILA_BOTS=[]

# 内部插件配置 -----------------------------

# FF14
FFLOGS_RANGE=14
FFLOGS_CACHE_TIME=4:30:00
Expand All @@ -27,15 +44,7 @@ HEWEATHER_KEY
# 复读
REPEAT_MIGRATION_GROUP_ID

# 机器人实现配置 -----------------------------

# Teydacore(telegram bot)
# https://github.com/teyda/teydacore
TELEGRAM_TOKEN=TELEGRAM_TOKEN
ONEBOT_WSR_URL=ws://coolqbot:8080/onebot/v12
ONEBOT_WSR_ACCESS_TOKEN=ACCESS_TOKEN

# 外部插件配置
# 外部插件配置 -----------------------------

# Treehelp
# https://github.com/he0119/nonebot-plugin-treehelp
Expand Down
7 changes: 2 additions & 5 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@
"justMyCode": false
},
{
"name": "CoolQBot Test - Single",
"name": "Debug Unit Test",
"type": "python",
"request": "launch",
"module": "pytest",
"args": ["-s", "-k", "${selectedText}"],
"console": "integratedTerminal",
"request": "test",
"justMyCode": false
}
]
Expand Down
6 changes: 6 additions & 0 deletions bot.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import nonebot
from nonebot.adapters.kaiheila import Adapter as KaiheilaAdapter
from nonebot.adapters.onebot.v11 import Adapter as OneBotV11Adapter
from nonebot.adapters.onebot.v12 import Adapter as OneBotV12Adapter
from nonebot.adapters.qqguild import Adapter as QQGuildAdapter
from nonebot.adapters.red import Adapter as RedAdapter
from nonebot.log import logger
from sqlalchemy import StaticPool

Expand All @@ -10,6 +13,9 @@
driver = nonebot.get_driver()
driver.register_adapter(OneBotV11Adapter)
driver.register_adapter(OneBotV12Adapter)
driver.register_adapter(KaiheilaAdapter)
driver.register_adapter(QQGuildAdapter)
driver.register_adapter(RedAdapter)

# 加载插件
nonebot.load_from_toml("pyproject.toml")
Expand Down
459 changes: 311 additions & 148 deletions poetry.lock

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,26 @@ version = "0.16.0"
python = "^3.11"
eorzeaenv = "^2.2.8"
matplotlib = "^3.7.1"
expiringdict = "^1.2.2"

nonebot2 = { extras = ["httpx", "fastapi"], version = "^2.0.0" }
nonebot2 = { extras = ["httpx", "fastapi", "websockets"], version = "^2.0.0" }
nb-cli = "^1.2.3"
nonebot-adapter-onebot = "2.2.4"
onebot-qqguild-extension = "^0.1.1"
nonebot-adapter-qqguild = "^0.2.5"
nonebot-adapter-kaiheila = "^0.2.12"
nonebot-adapter-red = "^0.1.0"

nonebot-plugin-datastore = "^1.1.1"
nonebot-plugin-wordcloud = "^0.5.2"
nonebot-plugin-treehelp = "^0.3.0"

nonebot-plugin-apscheduler = "^0.3.0"
nonebot-plugin-send-anything-anywhere = "^0.3.0"
nonebot-plugin-alconna = "^0.22.3"
nonebot-plugin-session = "^0.1.0"
nonebot-plugin-userinfo = "^0.1.0"

nonebot-plugin-sentry = "^0.4.0"
nonebot-plugin-memes = "0.4.7"
nonebot-bison = "0.8.2"
Expand All @@ -36,15 +45,27 @@ pytest-mock = "^3.6.1"
pytest-xdist = "^3.0.2"
pytest-asyncio = "^0.21.0"
respx = "^0.20.1"
freezegun = "^1.2.2"

[tool.nonebot]
adapters = [
{ name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" },
{ name = "OneBot V12", module_name = "nonebot.adapters.onebot.v12" },
{ name = "开黑啦", module_name = "nonebot.adapters.kaiheila" },
{ name = "QQ 频道", module_name = "nonebot.adapters.qqguild" },
{ name = "RedProtocol", module_name = "nonebot.adapters.red" },
]
plugin_dirs = ["src/plugins"]
plugins = [
"onebot_qqguild_extension",
"nonebot_plugin_apscheduler",
"nonebot_plugin_saa",
"nonebot_plugin_alconna",
"nonebot_plugin_sentry",
"nonebot_plugin_treehelp",
"nonebot_plugin_datastore",
"nonebot_plugin_session",
"nonebot_plugin_userinfo",
"nonebot_plugin_wordcloud",
"nonebot_plugin_memes",
"nonebot_bison",
Expand Down
97 changes: 97 additions & 0 deletions src/plugins/user/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import random
from typing import cast

from expiringdict import ExpiringDict
from nonebot_plugin_alconna import (
Alconna,
AlconnaQuery,
Args,
Option,
Query,
on_alconna,
)
from nonebot_plugin_session import SessionLevel

from .annotated import UserSession
from .utils import get_user, remove_bind, set_bind

user_cmd = on_alconna(Alconna("user"), use_cmd_start=True)


@user_cmd.handle()
async def _(session: UserSession):
await user_cmd.finish(
"\n".join(
[
f"用户 ID:{session.uid}",
f"用户名:{session.name}",
f"用户创建日期:{session.created_at}",
f"用户所在平台 ID:{session.pid}",
f"用户所在平台:{session.platform}",
]
)
)


tokens = cast(
dict[str, tuple[str, str, int, SessionLevel | None]],
ExpiringDict(max_len=100, max_age_seconds=300),
)


bind_cmd = on_alconna(
Alconna("bind", Option("-r"), Args["token?", str]), use_cmd_start=True
)


@bind_cmd.handle()
async def _(
session: UserSession,
token: str | None = None,
remove: Query[bool] = AlconnaQuery("r.value", default=False),
):
if remove.result:
result = await remove_bind(session.pid, session.platform)
if result:
await bind_cmd.finish("解绑成功")
else:
await bind_cmd.finish("不能解绑最初绑定的账号")

# 生成令牌
if not token:
token = f"nonebot/{random.randint(100000, 999999)}"
tokens[token] = (session.pid, session.platform, session.uid, session.level)
await bind_cmd.finish(
f"命令 bind 可用于在多个平台间绑定用户数据。绑定过程中,原始平台的用户数据将完全保留,而目标平台的用户数据将被原始平台的数据所覆盖。\n请确认当前平台是你的目标平台,并在 5 分钟内使用你的账号在原始平台内向机器人发送以下文本:\n/bind {token}\n绑定完成后,你可以随时使用「bind -r」来解除绑定状态。"
)

# 绑定流程
if token in tokens:
# 平台的相关信息
pid, platform, user_id, level = tokens.pop(token)
# 群内绑定的第一步,会在原始平台发送令牌
# 此时 pid 和 platform 为目标平台的信息
if level == SessionLevel.LEVEL2 or level == SessionLevel.LEVEL3:
token = f"nonebot/{random.randint(100000, 999999)}"
tokens[token] = (session.pid, session.platform, user_id, None)
await bind_cmd.finish(
f"令牌核验成功!下面将进行第二步操作。\n请在 5 分钟内使用你的账号在目标平台内向机器人发送以下文本:\n/bind {token}\n注意:当前平台是你的原始平台,这里的用户数据将覆盖目标平台的数据。"
)
# 群内绑定的第二步,会在目标平台发送令牌
# 此时 pid 和 platform 为原始平台的信息
# 需要重新获取其用户信息,然后将目标平台绑定至原始平台
elif level is None:
if session.uid != user_id:
await bind_cmd.finish("请使用最开始要绑定账号进行操作")

user = await get_user(pid, platform)
await set_bind(session.pid, session.platform, user.id)
await bind_cmd.finish("绑定成功")
# 私聊绑定时,会在原始平台发送令牌
# 此时 pid 和 platform 为目标平台的信息
# 直接将目标平台绑定至原始平台
elif level == SessionLevel.LEVEL1:
await set_bind(pid, platform, session.uid)
await bind_cmd.finish("绑定成功")
else:
await bind_cmd.finish("令牌不存在或已过期")
10 changes: 10 additions & 0 deletions src/plugins/user/annotated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import Annotated

from nonebot.params import Depends

from .depends import UserSession as _UserSession
from .depends import get_or_create_user, get_user_session
from .models import User as _User

User = Annotated[_User, Depends(get_or_create_user)]
UserSession = Annotated[_UserSession, Depends(get_user_session)]
43 changes: 43 additions & 0 deletions src/plugins/user/depends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from nonebot.matcher import Matcher
from nonebot.params import Depends
from nonebot_plugin_session import Session, SessionLevel, extract_session
from nonebot_plugin_userinfo import EventUserInfo, UserInfo

from . import utils
from .models import UserSession


async def get_or_create_user(
matcher: Matcher,
session: Session = Depends(extract_session),
user_info: UserInfo | None = EventUserInfo(),
):
"""获取一个用户,如果不存在则创建"""
if (
session.platform == "unknown"
or session.level == SessionLevel.LEVEL0
or not session.id1
):
await matcher.finish("用户相关功能暂不支持当前平台")
raise ValueError("用户相关功能暂不支持当前平台")

try:
user = await utils.get_user(session.id1, session.platform)
except ValueError:
user = await utils.create_user(
session.id1,
session.platform,
user_info and user_info.user_name or session.id1,
)

return user


async def get_user_session(
matcher: Matcher,
session: Session = Depends(extract_session),
user_info: UserInfo | None = EventUserInfo(),
):
"""获取用户会话"""
user = await get_or_create_user(matcher, session, user_info)
return UserSession(session, user_info, user)
50 changes: 50 additions & 0 deletions src/plugins/user/migrations/5cb13347bece_init_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""init db
Revision ID: 5cb13347bece
Revises:
Create Date: 2023-09-12 17:07:22.228191
"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "5cb13347bece"
down_revision = None
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"user_user",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"user_bind",
sa.Column("pid", sa.String(length=64), nullable=False),
sa.Column("platform", sa.String(length=32), nullable=False),
sa.Column("aid", sa.Integer(), nullable=False),
sa.Column("bid", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["aid"],
["user_user.id"],
),
sa.ForeignKeyConstraint(
["bid"],
["user_user.id"],
),
sa.PrimaryKeyConstraint("pid", "platform"),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("user_bind")
op.drop_table("user_user")
# ### end Alembic commands ###
Loading

0 comments on commit 405fad3

Please sign in to comment.