本文作者: YOOZIKI
本文链接: https://yooziki.github.io/2020/08/297095/

Python版本:py37

安装相关模块,源码在这里

pip install graia-application-mirai
pip install graia-broadcast --upgrade
pip install singledispatchmethod

添加选择器子模块

pip install graia-component-selector

mirai环境配置

自定义方式(推荐)

mirai仓库中shadow下找到对应的包体并下载

如果出现无法下载的情况下载文件时将https://github.com替换成http://github-proxy.yobot.win即可

cd /tmp
wget -N -O shadowsocks.tar.gz http://github-proxy.yobot.win/heweiye/Merlin_Shadowsocks/master/shadowsocks_3.8.0.tar.gz
tar -zxvf /tmp/shadowsocks.tar.gz
chmod +x /tmp/shadowsocks/install.sh
sh /tmp/shadowsocks/install.sh

经测试的可用版本:

console = "mirai-console-1.0-M4-dev-3.jar"
pure = "mirai-console-pure-1.0-M4-dev-3.jar"
qqandroid = "mirai-core-qqandroid-1.2.3.jar"
mirai-api-http = "mirai-api-http-v1.8.2.jar"
java = "jdk8u265-b01"

使用简易的python启动器即可启动,首次启动后会创建一系列文件夹,使用stop命令正常停止,将mirai-api-http放到plugins下,再次启动plugins会自动生成配置文件存放在config\MiraiAPIHTTP下,使用文本方式打开后可以修改其中的一些配置,完成后再次用stop命令正常停止,之后使用:login user passwd登录即可

强行停止mirai会造成mirai-api-http没有结束的问题,如果出现请在任务管理器中关闭OpenJDK Platform binary进程

建议只修改autuKey增加安全性,其它改动可能会造成Graia无法连接

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

from os import system, path, makedirs, listdir

content = "./content"

console = "mirai-console-1.0-M4-dev-3.jar"
pure = "mirai-console-pure-1.0-M4-dev-3.jar"
qqandroid = "mirai-core-qqandroid-1.2.3.jar"
jar_name = {console, pure, qqandroid}

def init(content, jar_name):
print("初始化中...")
if not path.exists(content):
makedirs(content)
raise ValueError(f"请在 {content} 文件夹中放入 {console}, {pure}, {qqandroid} 可在 'https://github.com/project-mirai/mirai-repo' 处下载")

c = set()
for i in listdir(content):
if i in (console, pure, qqandroid):
c.add(i)

if len(c) < 3:
raise ValueError("目录 {content} 中缺少↓ \n{data}\n可在 'https://github.com/project-mirai/mirai-repo' 处下载".format(
content=content,
data=', '.join(jar_name - c)
))
print("开始启动 mirai-console ...")

def run(content, JClass):
try:
system(f"java -cp {content}/* {JClass}")
except KeyboardInterrupt:
print("手动关闭 mirai...")

if __name__ == "__main__":
init(content, jar_name)
run(content, '"net.mamoe.mirai.console.pure.MiraiConsolePureLoader"')

mirua方式(一键懒人包)

可以在这里获得

git colne https://github.com/ieew/mirua

miraiOK方式

首先找到miraiOK仓库或者使用我的fork仓库并且根据说明文件下载可执行程序到miraoOK目录下

git clone https://github.com/LXY1226/MiraiOK

执行对应的可执行文件,等待第一次执行结束

为了保证程序的正确性,第一次执行结束后,请创建一个.nonupdate文件禁用更新功能,或者可以试试更新一次会不会出问题

对应的mirai相关jar包可以在这里查看

执行第一次之后,在miraiOK的目录下会有一个plugins文件夹,将mirai-api-http-version.jar包放到这里,插件源码

如果miraiOK给到的./content/中mirai-core版本为1.0一下,则只能使用1.7版本的mirai-api-http

为了保证py程序的正确运行,再次运行可执行文件

然后回到plugins文件夹下,新生成了一个文件夹,里面有一个yml配置文件,使用txt方式打开即可。(如果没有生成文件夹,则手动创建./MiraiAPIHTTP/setting.yml)

写入

##该配置为分段配置,对所有会话有效

#Graia只能是0.0.0.0
主机: “ 0.0.0.0 ”

#
端口: 8080

#Graia需要手动指定,并且和py中保持一致
authKey: 123456780

#任选,缓存大小,预设4096.缓存过小会导致引用回复和撤回消息失败
cacheSize: 4096

#任选,是否开启websocket,更改关闭,建议通过会话范围的配置设置
enableWebsocket: true

#任选,配置CORS跨域,交替为*,即允许所有,可以不做更改
域名cors:
- ' * '

如果希望每次都自动登录,可以在miraiOK更目录下的config.txt中根据模板提示写入内容

程序框架

可以从这里查看

# 所有事件监听都在entry中可以找到
from graia.application.entry import (
GraiaMiraiApplication, Session,
MessageChain,Group,Friend,Member,MemberInfo,
Plain,Image,AtAll,At,Face
)
from graia.application.entry import (
BotMuteEvent,BotGroupPermissionChangeEvent
)
from graia.broadcast import Broadcast
#from graia.template import Template # 模板功能
#from graia.component import Components # 检索器

import asyncio
from pathlib import Path
# 监听
loop = asyncio.get_event_loop()

bcc = Broadcast(loop=loop)
app = GraiaMiraiApplication(
broadcast=bcc,
connect_info=Session(
host="http://localhost:8080", # 填入 httpapi 服务运行的地址,如果根据我的配置的话可以不做修改
authKey="1234567890", # 填入 authKey
account=1111111, # 你的机器人的 qq 号
websocket=True # Graia 已经可以根据所配置的消息接收的方式来保证消息接收部分的正常运作.
)
)

其中的bcc是一个广播,会将从miraiOK获得的信息在程序内广播传递。

app是一个像mirai框架传递信息的通道

# 写入监听事件handler
@bcc.receiver("FriendMessage")
async def friend_message_listener(app: GraiaMiraiApplication, friend: Friend):
await app.sendFriendMessage(friend, MessageChain.create([
Plain("谢谢, 非常感谢你对我们服务的满意.")
]))
# 程序固定尾
app.launch_blocking()

API

MessageChain

from graia.application.message.chain import MessageChain

用于处理消息的数据结构,包含一个有序列表

  • MessageChain.asDisplay()
    得到字符串形式的消息表示,返回字符串
  • MessageChain.root
    返回消息链列表
  • MessageChain.doc
    返回消息链的API文档说明
  • MessageChain.module
    返回消息链的模块绝对引用
  • MessageChain.create(<list/tuple>)
    创建一个消息链结构,其中list/tuple可以包含能够获取的类型元素,返回创建的消息链
  • MessageChain.isImmutable()
    判断消息链是否为可变,可变返回True,不可变返回False,默认接受的消息为不可变
  • MessageChain.asMutable()
    将消息链转换为可变的,返回可变的消息链
  • MessageChain.isSendable()
    检查消息链是否可以被正确发送,可发送返回True,不可发送返回False
  • MessageChain.asSendable()
    将消息链转换为可以发送的新消息链,返回可能可以正确发送的消息链
  • MessageChain.has(T)
    消息链中是否存在某种消息元素,亦可使用T in MessageChain
    如果有则返回True否则返回False
  • MessageChain.get(T)
    取出消息链中某种消息元素,以列表方式返回
    亦可以使用MessageChain[T]
  • MessageChain.join(*MessageChains)
    将多个MessageChain按顺序拼合“
    返回拼合后的消息链
  • MessageChain.plusWith(*MessageChain)
    方法将在现有的基础上将另一消息链拼接到原来实例的尾部, 并生成, 返回新的实例; 该方法不改变原有和传入的实例.
  • MessageChain.plus(*MessageChain)
    方法将在现有的基础上将另一消息链拼接到原来实例的尾部; 该方法更改了原有的实例, 并要求 isMutable 方法返回 True 才可以执行.
  • MessageChain.asSerializationString()
    将消息链对象转为以 “Mirai 码” 表示特殊对象的字符串
    返回字符串
  • MessageChain.fromSerializationString(string)
    将以 “Mirai 码” 表示特殊对象的字符串解析为消息链, 不过可能不完整
    返回消息链,不可变
  • MessageChain.asMerged()
    把相邻的Plain元素合并为一个Plain元素
  • MessageChain.subchain(slice)
    切片操作
    类似list切片方式
    返回切片后的MessageChain
  • MessageChain.exclude(T)
    将消息链中某种元素排除
    返回新的消息链
  • MessageChain.include(T)
    只保留消息链中某种元素
    返回新消息链

Elements

import graia.application.message.elements.internal as Elements
  • Elements.Plain(string)
    实例化一个Plain元素,用于承载消息中的文字

  • Elements.Source
    表示消息在一个特定聊天区域内的唯一标识,收到的消息链中存在,只能够获取

  • Elements.Quote(id,groupID,senderID,targetID,origin)
    表示消息中回复功能,
    id:回复的消息在群内的唯一标识,通过Elements.Source得到
    groupID:群号
    senderID:原消息发送人
    targetID:群号
    oringin:发送的消息链

    消息链中应该有:原消息链的root列表, At(type=‘At’,target=qq号,display=’@谁谁谁(群内id)’), Plain(’ ‘),Plain(“内容”)

  • Elements.At(id)
    表示@某人

  • Elements.AtAll()
    管理员有效,@全体成员

  • Elements.Face(id)
    表示一个表现,QQ内置的表情,id是表情的id

  • Elements.ImageType(Enum)
    Friend = “Friend”
    Group = “Group”
    Temp = “Temp”
    Unknown = “Unknow”
    是一个类型的定义,不需要直接调用

  • Elements.Image()
    imageID:是一个16进制字符串
    url:链接位置
    path
    type:图片属于哪里(ImageType定义)
    [关于Image的方法](# Image)

  • Elements.FlashImage(Image)
    发送闪照,需要先通过Image方法创建一个Image对象
    亦可以直接Image.asFlash()

  • Elements.Xml(str)
    xml消息

  • Elements.Json(str)

  • Elements.App(str)
    app消息

  • Elements.PokeMethods(Enum)
    是一个类型定义,不需要直接调用
    Poke = “Poke”
    ShowLove = “ShowLove”
    Like =“Like”
    Heartbroken=“Heartbroken”
    SixSixSix=“SixSixSix”
    FangDaZhao = “FangDaZhao”

  • Elements.Poke(PokeMethods)

Image

from graia.application.message.elements.internal import Image

[返回到Elements](# Elements)

  • Image.fromLoacalFile(filepath)
    从本地获取一张图片
    返回ShadowElement,包含了一个asFlash方法,可以作为闪照发送Image.fromLocalFile(path).asFlash()
  • Image.fromUnsafePath(filepath)
    不检查路径安全性,让上层协议(mirai-api-http)处理图片文件
  • Image.fromUnsafeBytes()
    从不保证有效性的bytes中创建一个ShadowElement,并且上传,同样包含一个asFlash方法
    不推荐,安全性低
  • Image.fromNetworkAddress(url)
    从不保证有效性的网络位置中创建一个ShadowElement,在发送的时候从url获得图片并且上传
    可能抛出任何形式的网络错误
    可以使用asFlash方法
  • Image.fromUnsafeAddress(url)
    让上层协议(mirai-apii-http)处理图片文件
  • Image.asDisplay()
    返回字符串”[图片]”
  • Image.http_to_bytes(url)
    从远端服务器获取图片的 bytes, 注意, 你无法获取并不包含 url 属性的本元素的 bytes
  • Image.asFlash()
    设为闪照发送
  • Image.asSerializationString()
    将消息链对象转为以 “Mirai 码” 表示特殊对象的字符串

Massages

定义了几种消息的类型

import graia.application.event.messages as Messages
  • FriendMessage
    好友对话事件
  • GroupMessage
    群对话事件
  • TempMessage
    临时对话事件

Friend模块

定义了friend的几种属性,需要在监听中声明

from graia.application.friend import Friend
  • id:qq号
  • nickname:昵称
  • remark:好像访问不到??

Group模块

定义了group的一些属性,需要在监听中声明,例如:async def group_message_handler(app: GraiaMiraiApplication, message: MessageChain, group: Group,member: Member):

import graia.application.group as Group

Group.Group

Group.MemberPerm

作为字典枚举,并不需要主动调用

  • .Member # 普通成员
  • .Administrator # 管理员
  • .Owner # 群主

Group.Member

描述用户在群组中所具备的有关状态,包括所在群组,群中昵称,所具备的权限,唯一的ID

  • .id:说话人的QQ号
  • .name:说话人的群昵称
  • .permission:说话人的权限等级
  • .group:上面的Group.Group所有内容

Group.GroupConfig

描述群内各项功能的设置(使用方法不明)

  • name
  • announcement
  • confessTalk
  • allowMemberInvite
  • autoApprove
  • anonymousChat

Group.MemberInfo

描述群组成员的可修改状态,修改需要权限(使用方法不明)

  • name
  • specialTitle

GraiaMiraiApplication类

大多数的功能都被存在这个类中

from graia.application import GraiaMiraiApplication

.getVersion()

返回mirai-api-http的版本

.getGroup(int)

返回对应群号的群组信息(为空则None)
id,name,accountPermprint(app.getGroup(<Id>))

.groupList()

print(await app.groupList())

.getMember(Group int,member int)

从已知群组和已知成员的id获得成员的信息

print(await app.getMember(<ID>,<ID>))

.memberList(Group int)

从已知群组中获取群内所有成员的信息列表

.friendList()

获取所有好友的信息列表

print(await app.friendList())

.getFriend(int)

根据好友的id获取Friend实例

.sendFriendMessage(Friend,MessageChain[,quote])

向好友发送消息,quote为回复信息断,可以选择

.sendGroupMessage(Group,MessageChain[,quote])

向群组发送消息

.sendTempMessage(Group/ID,Member/ID,messageChain[,quote])

向群内成员发起临时会话

.revokeMessage(target)

撤回消息(2min以内)

target (Union[Source, BotMessage, int]): 特定信息的 messageId, 可以是 Source 实例, BotMessage 实例或者是单纯的 int 整数.

.messageFromId(source[Source, int])

尝试从已知的 messageId 获取缓存中的消息

.muteAll(group)&.unmuteAll(group)

全员禁言

.mute(group,member,time)、unmute(group,membder)

禁言指定群友

time指禁言时长

.kick(group,membder)

群内踢掉某人

.quit(group)

退群

Event模块

通过Graia.application.entry可以导入MiraiApiHttp支持的所有事件类型

from graia.application.entry import (
*
)
@bcc.receiver("xxxEvent")
async def function(event:xxxEvent):
print("EventHappened")
event.<可选获得参数/方法>

BotOnlineEvent

当该事件发生时, 应用实例所辖账号登录成功
qq: int

BotOfflineEventActive

当该事件发生时, 应用实例所辖账号主动离线
qq: int

BotOfflineEventForce

当该事件发生时, 应用实例所辖账号被迫离线
qq: int

BotOfflineEventDropped

当该事件发生时, 应用实例所辖账号与服务器的连接被服务器主动断开, 或因网络原因离线
qq: int

BotReloginEvent

当该事件发生时, 应用实例所辖账号正尝试重新登录
qq: int

BotGroupPermissionChangeEvent

当该事件发生时, 应用实例所辖账号在一特定群组内所具有的权限发生变化
origin: MemberPerm
current: MemberPerm
group: Group

BotMuteEvent

当该事件发生时, 应用实例所辖账号在一特定群组内被管理员/群主禁言

Allowed Extra Parameters(提供的额外注解支持):
GraiaMiraiApplication (annotation): 发布事件的应用实例
Member (annotation, optional = None): 执行解除禁言操作的管理员/群主, 若为 None 则为应用实例所辖账号操作
Group (annotation, optional = None): 发生该事件的群组

durationSeconds: int
operator: Optional[Member]

BotUnmuteEvent

当该事件发生时, 应用实例所辖账号在一特定群组内被管理员/群主解除禁言
operator: Optional[Member]

BotJoinGroupEvent

当该事件发生时, 应用实例所辖账号加入指定群组
group: Group

BotLeaveEventActive

当该事件发生时, 应用实例所辖账号主动退出了某群组.
group: Group

BotLeaveEventKick

当该事件发生时, 应用实例所辖账号被某群组的管理员/群主从该群组中删除.
group: Group

GroupRecallEvent

当该事件发生时, 有群成员在指定群组撤回了一条消息
注意, 这里的群成员若具有管理员/群主权限, 则他们可以撤回其他普通群员的消息, 且不受发出时间限制
authorId: int
messageId: int
time: datetime
group: Group
operator: Optional[Member]

FriendRecallEvent

当该事件发生时, 有一位与应用实例所辖账号为好友关系的用户撤回了一条消息
authorId: int
messageId: int
time: int
operator: int

GroupNameChangeEvent

该事件发生时, 有一群组被修改了群名称
origin: str
current: str
group: Group
operator: Optional[Member] = None

GroupEntranceAnnouncementChangeEvent

该事件发生时, 有一群组被修改了入群公告
origin: str
current: str
group: Group
operator: Optional[Member]

GroupMuteAllEvent

该事件发生时, 有一群组开启了全体禁言
origin: bool
current: bool
group: Group
operator: Optional[Member]

GroupAllowAnonymousChatEvent

该事件发生时, 有一群组修改了有关匿名聊天的相关设定
origin: bool
current: bool
group: Group
operator: Optional[Member]

GroupAllowConfessTalkEvent

该事件发生时, 有一群组修改了有关坦白说的相关设定
origin: bool
current: bool
group: Group
isByBot: bool

GroupAllowMemberInviteEvent

该事件发生时, 有一群组修改了有关是否允许已有成员邀请其他用户加入群组的相关设定
origin: bool
current: bool
group: Group
operator: Optional[Member]

MemberJoinEvent

该事件发生时, 有一新成员加入了一特定群组
member: Member

MemberLeaveEventKick

该事件发生时, 有一群组成员被管理员/群主从群组中删除, 当 operatorNone 时, 执行者为应用实例所辖账号.
type = “MemberLeaveEventKick”
member: Member
operator: Optional[Member]

MemberLeaveEventQuit

该事件发生时, 有一群组成员主动退出群组.
member: Member

MemberCardChangeEvent

该事件发生时, 有一群组成员的群名片被更改, 执行者可能是管理员/群主, 该成员自己, 也可能是应用实例所辖账号(这时, operatorNone).
origin: str
current: str
member: Member
operator: Optional[Member]

MemberSpecialTitleChangeEvent

该事件发生时, 有一群组成员的群头衔被更改, 执行者只可能是群组的群主.
origin: str
current: str
member: Member

MemberPermissionChangeEvent

该事件发生时, 有一群组成员的权限被更改/调整, 执行者只可能是群组的群主.
origin: str
current: str
member: Member

MemberMuteEvent

该事件发生时, 有一群组成员被管理员/群组禁言, 当 operatorNone 时为应用实例所辖账号操作.
durationSeconds: int
member: Member
operator: Optional[Member]

MemberUnmuteEvent

该事件发生时, 有一群组成员被管理员/群组解除禁言, 当 operatorNone 时为应用实例所辖账号操作.
member: Member
operator: Optional[Member]

NewFriendRequestEvent

当该事件发生时, 有一用户向机器人提起好友请求.

该事件的处理需要你获取原始事件实例.

  1. 读取该事件的基础信息:
event.supplicant: int # 发起加好友请求的用户的 ID
event.sourceGroup: Optional[int] # 对方可能是从某个群发起对账号的请求的, mirai 可以解析对方从哪个群发起的请求.
event.nickname: str # 对方的昵称
event.message: str # 对方发起请求时填写的描述
同意请求: `await event.accept()`, 具体查看该方法所附带的说明.
拒绝请求: `await event.reject()`, 具体查看该方法所附带的说明.
拒绝并不再接受来自对方的请求: `await event.rejectAndBlock()`, 具体查看该方法所附带的说明.

requestId: int = Field(…, alias=“eventId”)
supplicant: int = Field(…, alias=“fromId”) # 即请求方 QQ
sourceGroup: Optional[int] = Field(…, alias=“groupId”)
nickname: str = Field(…, alias=“nick”)
message: str

accept(message:str=””)

同意对方的加好友请求.
message (str, optional): 附带给对方的消息. 默认为 “”.

reject(message: str = “”)

拒绝对方的加好友请求.
message (str, optional): 附带给对方的消息. 默认为 “”.

rejectAndBlock(message: str = “”)

拒绝对方的加好友请求, 并不再接受来自对方的加好友请求.
message (str, optional): 附带给对方的消息. 默认为 “”.

MemberJoinRequestEvent

当该事件发生时, 有一用户向机器人作为管理员/群主的群组申请加入群组.

该事件的处理需要你获取原始事件实例.

  1. 读取该事件的基础信息:
    5event.supplicant: int # 申请加入群组的用户的 ID
    event.groupId: Optional[int] # 对方试图加入的群组的 ID
    event.groupName: str # 对方试图加入的群组的名称
    event.nickname: str # 对方的昵称
    event.message: str # 对方发起请求时填写的描述
    同意请求: `await event.accept()`, 具体查看该方法所附带的说明.
    拒绝请求: `await event.reject()`, 具体查看该方法所附带的说明.
    忽略请求: `await event.ignore()`, 具体查看该方法所附带的说明.
    拒绝并不再接受来自对方的请求: `await event.rejectAndBlock()`, 具体查看该方法所附带的说明.
    忽略并不再接受来自对方的请求: `await event.ignoreAndBlock()`, 具体查看该方法所附带的说明.

requestId: int = Field(…, alias=“eventId”)
supplicant: int = Field(…, alias=“fromId”) # 即请求方 QQ
groupId: Optional[int] = Field(…, alias=“groupId”)
groupName: str = Field(…, alias=“groupName”)
nickname: str = Field(…, alias=“nick”)
message: str

accept(message: str = “”)

同意对方加入群组.
message (str, optional): 附带给对方的消息. 默认为 “”.

reject(message: str = “”)

拒绝对方加入群组.
message (str, optional): 附带给对方的消息. 默认为 “”.

ignore(self, message: str = “”)

忽略对方加入群组的请求.
message (str, optional): 附带给对方的消息. 默认为 “”.

rejectAndBlock(self, message: str = “”)

拒绝对方加入群组的请求, 并不再接受来自对方加入群组的请求
message (str, optional): 附带给对方的消息. 默认为 “”.

BotInvitedJoinGroupRequestEvent

当该事件发生时, 应用实例所辖账号接受到来自某个账号的邀请加入某个群组的请求.

该事件的处理需要你获取原始事件实例.

  1. 读取该事件的基础信息:
    int # 邀请所辖账号加入群组的用户的 ID
    event.groupId: Optional[int] # 对方邀请所辖账号加入的群组的 ID
    event.groupName: str # 对方邀请所辖账号加入的群组的名称
    event.nickname: str # 对方的昵称
    event.message: str # 对方发起请求时填写的描述
    同意请求: `await event.accept()`, 具体查看该方法所附带的说明.
    拒绝请求: `await event.reject()`, 具体查看该方法所附带的说明.

requestId: int = Field(…, alias=“eventId”)
supplicant: int = Field(…, alias=“fromId”) # 即请求方 QQ
groupId: Optional[int] = Field(…, alias=“groupId”)
groupName: str = Field(…, alias=“groupName”)
nickname: str = Field(…, alias=“nick”)
message: str

accept(message: str = “”)

同意对方加入群组.
message (str, optional): 附带给对方的消息. 默认为 “”.

reject(message: str = “”)

拒绝对方加入群组.
message (str, optional): 附带给对方的消息. 默认为 “”.

GraiaScheduler模块

from graia.scheduler import (
timers,
)
import graia.scheduler as scheduler
loop = asyncio.get_event_loop()
bcc = Broadcast(loop=loop)
app = GraiaMiraiApplication(
...
)
sche = scheduler.GraiaScheduler(loop=loop,broadcast=bcc)

....
@sche.schedule(timers.every_custom_seconds(60))
async def test():
print("60s一次")
....

timers为一些常用的Scheduler时间设置

对于timers中自造方法,使用croniter模块

from datetime import datetime, timedelta
from croniter import croniter

def every(**kwargs):
while True:
yield datetime.now() + timedelta(**kwargs)

def func(n):
"""每时间执行一次"""
yield from every(seconds=n)
#yield from every(hours=n)
#yield from every(minutes=n)

def crontabify(pattern: str):
"""使用类似 crontab 的方式生成计时器

Args:
pattern (str): crontab 的设置, 具体请合理使用搜索引擎
"""
base_datetime = datetime.now()
crontab_iter = croniter(pattern, base_datetime)
while True:
yield crontab_iter.get_next(datetime)