-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #84 from AmiyaBot/dev
Dev
- Loading branch information
Showing
21 changed files
with
688 additions
and
120 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
name: Pylint | ||
|
||
on: | ||
push: | ||
branches: | ||
- master | ||
|
||
jobs: | ||
pypi-publish: | ||
name: upload release to PyPI | ||
runs-on: ubuntu-latest | ||
environment: release | ||
permissions: | ||
id-token: write | ||
steps: | ||
- name: Check out the repository | ||
uses: actions/checkout@v2 | ||
|
||
- name: Set up Python | ||
uses: actions/setup-python@v2 | ||
with: | ||
python-version: '3.8' | ||
|
||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install setuptools wheel twine | ||
- name: Build package | ||
run: | | ||
python setup.py bdist_wheel --auto-increment-version | ||
- name: Publish package distributions to PyPI | ||
uses: pypa/gh-action-pypi-publish@release/v1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
/.idea/ | ||
/__pycache__/ | ||
/resource/ | ||
/plugins/ | ||
/build/ | ||
/dist/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
import os | ||
import base64 | ||
|
||
from graiax import silkcoder | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import asyncio | ||
|
||
from typing import Optional | ||
from amiyabot.builtin.messageChain import Chain, ChainBuilder | ||
from amiyabot.adapters.tencent.qqGuild import QQGuildBotInstance | ||
|
||
from .api import QQGroupAPI, log | ||
from .builder import QQGroupMessageCallback, QQGroupChainBuilder, QQGroupChainBuilderOptions, build_message_send | ||
from .package import package_qq_group_message | ||
from ... import HANDLER_TYPE | ||
|
||
|
||
def qq_group( | ||
client_secret: str, | ||
default_chain_builder: Optional[ChainBuilder] = None, | ||
default_chain_builder_options: QQGroupChainBuilderOptions = QQGroupChainBuilderOptions(), | ||
): | ||
def adapter(appid: str, token: str): | ||
if default_chain_builder: | ||
cb = default_chain_builder | ||
else: | ||
cb = QQGroupChainBuilder(default_chain_builder_options) | ||
|
||
return QQGroupBotInstance(appid, token, client_secret, cb) | ||
|
||
return adapter | ||
|
||
|
||
class QQGroupBotInstance(QQGuildBotInstance): | ||
def __init__(self, appid: str, token: str, client_secret: str, default_chain_builder: ChainBuilder): | ||
super().__init__(appid, token) | ||
|
||
self.__access_token_api = QQGroupAPI(self.appid, self.token, client_secret) | ||
self.__default_chain_builder = default_chain_builder | ||
|
||
def __str__(self): | ||
return 'QQGroup' | ||
|
||
@property | ||
def api(self): | ||
return self.__access_token_api | ||
|
||
@property | ||
def package_method(self): | ||
return package_qq_group_message | ||
|
||
async def start(self, private: bool, handler: HANDLER_TYPE): | ||
if hasattr(self.__default_chain_builder, 'start'): | ||
self.__default_chain_builder.start() | ||
|
||
await super().start(private, handler) | ||
|
||
async def send_chain_message(self, chain: Chain, is_sync: bool = False): | ||
if chain.use_default_builder: | ||
chain.builder = self.__default_chain_builder | ||
|
||
payloads = await build_message_send(self.api, chain) | ||
res = [] | ||
|
||
for payload in payloads: | ||
async with log.catch('post error:', ignore=[asyncio.TimeoutError]): | ||
res.append( | ||
await self.api.post_group_message( | ||
chain.data.channel_openid, | ||
payload, | ||
) | ||
) | ||
|
||
return [QQGroupMessageCallback(chain.data, self, item) for item in res] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import json | ||
import time | ||
import requests | ||
|
||
|
||
from ..qqGuild.api import QQGuildAPI, log | ||
|
||
|
||
class QQGroupAPI(QQGuildAPI): | ||
def __init__(self, appid: str, token: str, client_secret: str): | ||
super().__init__(appid, token) | ||
|
||
self.appid = appid | ||
self.client_secret = client_secret | ||
self.access_token = '' | ||
self.expires_time = 0 | ||
|
||
@property | ||
def headers(self): | ||
if not self.access_token or self.expires_time - time.time() <= 60: | ||
try: | ||
res = requests.post( | ||
url='https://bots.qq.com/app/getAppAccessToken', | ||
data=json.dumps( | ||
{ | ||
'appId': self.appid, | ||
'clientSecret': self.client_secret, | ||
} | ||
), | ||
headers={ | ||
'Content-Type': 'application/json', | ||
}, | ||
timeout=3, | ||
) | ||
data = json.loads(res.text) | ||
|
||
self.access_token = data['access_token'] | ||
self.expires_time = int(time.time()) + int(data['expires_in']) | ||
|
||
except Exception as e: | ||
log.error(e, desc='accessToken requests error:') | ||
|
||
return { | ||
'Authorization': f'QQBot {self.access_token}', | ||
'X-Union-Appid': f'{self.appid}', | ||
} | ||
|
||
@property | ||
def domain(self): | ||
return 'https://api.sgroup.qq.com' | ||
|
||
async def upload_file(self, channel_openid: str, file_type: int, url: str, srv_send_msg: bool = False): | ||
return await self.post( | ||
f'/v2/groups/{channel_openid}/files', | ||
{ | ||
'file_type': file_type, | ||
'url': url, | ||
'srv_send_msg': srv_send_msg, | ||
}, | ||
) | ||
|
||
async def post_group_message(self, channel_openid: str, payload: dict): | ||
return await self.post(f'/v2/groups/{channel_openid}/messages', payload) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import time | ||
import shutil | ||
|
||
from graiax import silkcoder | ||
from dataclasses import asdict, field | ||
from amiyabot.util import create_dir, get_public_ip, random_code, Singleton | ||
from amiyabot.adapters import MessageCallback | ||
from amiyabot.network.httpServer import HttpServer | ||
from amiyabot.builtin.messageChain import Chain | ||
from amiyabot.builtin.messageChain.element import * | ||
|
||
from .api import QQGroupAPI, log | ||
|
||
|
||
@dataclass | ||
class GroupPayload: | ||
content: str = '' | ||
msg_type: int = 0 | ||
markdown: Optional[dict] = None | ||
keyboard: Optional[dict] = None | ||
media: Optional[dict] = None | ||
ark: Optional[dict] = None | ||
image: Optional[str] = None | ||
message_reference: Optional[dict] = None | ||
event_id: Optional[str] = None | ||
msg_id: Optional[str] = None | ||
msg_seq: Optional[int] = None | ||
|
||
|
||
@dataclass | ||
class QQGroupChainBuilderOptions: | ||
host: str = '0.0.0.0' | ||
port: int = 8086 | ||
resource_path: str = './resource' | ||
http_server_options: dict = field(default_factory=dict) | ||
|
||
|
||
class QQGroupChainBuilder(ChainBuilder, metaclass=Singleton): | ||
def __init__(self, options: QQGroupChainBuilderOptions): | ||
create_dir(options.resource_path) | ||
|
||
self.server = HttpServer(options.host, options.port, **options.http_server_options) | ||
self.server.add_static_folder('/resource', options.resource_path) | ||
|
||
self.ip = options.host if options.host != '0.0.0.0' else get_public_ip() | ||
self.http = 'https' if self.server.server.config.is_ssl else 'http' | ||
self.options = options | ||
|
||
self.file_caches = {} | ||
|
||
@property | ||
def domain(self): | ||
return f'{self.http}://{self.ip}:{self.options.port}/resource' | ||
|
||
def start(self): | ||
asyncio.create_task(self.server.serve()) | ||
|
||
def temp_filename(self, suffix: str): | ||
filename = f'{int(time.time())}{random_code(10)}.{suffix}' | ||
path = f'{self.options.resource_path}/{filename}' | ||
url = f'{self.domain}/{filename}' | ||
|
||
create_dir(path, is_file=True) | ||
|
||
self.file_caches[url] = path | ||
|
||
return path, url | ||
|
||
def remove_file(self, url: str): | ||
if url in self.file_caches: | ||
os.remove(self.file_caches[url]) | ||
del self.file_caches[url] | ||
|
||
async def get_image(self, image: Union[str, bytes]) -> Union[str, bytes]: | ||
if isinstance(image, bytes): | ||
path, url = self.temp_filename('png') | ||
|
||
with open(path, mode='wb') as f: | ||
f.write(image) | ||
|
||
return url | ||
return image | ||
|
||
async def get_voice(self, voice_file: str) -> str: | ||
voice = await silkcoder.async_encode(voice_file, ios_adaptive=True) | ||
path, url = self.temp_filename('silk') | ||
|
||
with open(path, mode='wb') as f: | ||
f.write(voice) | ||
|
||
return url | ||
|
||
async def get_video(self, video_file: str) -> str: | ||
path, url = self.temp_filename('mp4') | ||
shutil.copy(video_file, path) | ||
return url | ||
|
||
|
||
class QQGroupMessageCallback(MessageCallback): | ||
async def recall(self): | ||
... | ||
|
||
async def get_message(self): | ||
... | ||
|
||
|
||
async def build_message_send(api: QQGroupAPI, chain: Chain, custom_chain: Optional[CHAIN_LIST] = None): | ||
chain_list = custom_chain or chain.chain | ||
|
||
payload_list: List[GroupPayload] = [] | ||
payload = GroupPayload(msg_id=chain.data.message_id) | ||
|
||
async def insert_media(url: str, file_type: int = 1): | ||
nonlocal payload | ||
|
||
if not isinstance(url, str): | ||
log.warning(f'unsupported file type "{type(url)}".') | ||
return | ||
|
||
if url.startswith('http'): | ||
res = await api.upload_file(chain.data.channel_openid, file_type, url) | ||
if res: | ||
if 'file_info' in res.json: | ||
file_info = res.json['file_info'] | ||
|
||
payload.msg_type = 7 | ||
payload.media = {'file_info': file_info} | ||
|
||
payload_list.append(payload) | ||
payload = GroupPayload(msg_id=chain.data.message_id) | ||
else: | ||
log.warning('file upload fail.') | ||
|
||
if isinstance(chain.builder, QQGroupChainBuilder): | ||
chain.builder.remove_file(url) | ||
else: | ||
log.warning(f'media file must be network paths.') | ||
|
||
for item in chain_list: | ||
# Text | ||
if isinstance(item, Text): | ||
payload.content += item.content | ||
|
||
# Image | ||
if isinstance(item, Image): | ||
await insert_media(await item.get()) | ||
|
||
# Voice | ||
if isinstance(item, Voice): | ||
await insert_media(await item.get(), 3) | ||
|
||
# Video | ||
if isinstance(item, Video): | ||
await insert_media(await item.get(), 2) | ||
|
||
# Html | ||
if isinstance(item, Html): | ||
await insert_media(await item.create_html_image()) | ||
|
||
if payload.content: | ||
payload_list.append(payload) | ||
|
||
return [asdict(item) for item in payload_list] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from amiyabot.builtin.message import Event, Message | ||
from amiyabot.adapters import BotAdapterProtocol | ||
from amiyabot.adapters.common import text_convert | ||
|
||
|
||
async def package_qq_group_message(instance: BotAdapterProtocol, event: str, message: dict, is_reference: bool = False): | ||
message_created = [ | ||
'C2C_MESSAGE_CREATE', | ||
'GROUP_AT_MESSAGE_CREATE', | ||
] | ||
if event in message_created: | ||
data = Message(instance, message) | ||
data.is_direct = event == 'C2C_MESSAGE_CREATE' | ||
|
||
data.user_id = message['author']['id'] | ||
data.user_openid = message['author']['member_openid'] | ||
data.channel_id = message['group_id'] | ||
data.channel_openid = message['group_openid'] | ||
data.message_id = message['id'] | ||
|
||
if 'content' in message: | ||
data = text_convert(data, message['content'].strip(), message['content']) | ||
|
||
return data | ||
|
||
return Event(instance, event, message) |
Oops, something went wrong.