Skip to content

Commit

Permalink
Merge pull request #84 from AmiyaBot/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
vivien8261 authored Jan 5, 2024
2 parents e5258b3 + 9308d31 commit dad89c9
Show file tree
Hide file tree
Showing 21 changed files with 688 additions and 120 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/pypi.yml
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/.idea/
/__pycache__/
/resource/
/plugins/
/build/
/dist/
Expand Down
2 changes: 1 addition & 1 deletion amiyabot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from amiyabot.builtin.lib.browserService import BrowserLaunchConfig, basic_browser_service

# message
from amiyabot.builtin.messageChain import Chain, ChainBuilder
from amiyabot.builtin.messageChain import Chain, ChainBuilder, InlineKeyboard
from amiyabot.builtin.message import (
Event,
EventList,
Expand Down
4 changes: 0 additions & 4 deletions amiyabot/adapters/kook/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,6 @@ async def make_image_message(data):
if isinstance(item, Image):
await make_image_message(await item.get())

# Voice
if isinstance(item, Voice):
pass

# Html
if isinstance(item, Html):
result = await item.create_html_image()
Expand Down
1 change: 0 additions & 1 deletion amiyabot/adapters/onebot/v11/builder.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
import base64

from graiax import silkcoder
Expand Down
69 changes: 69 additions & 0 deletions amiyabot/adapters/tencent/qqGroup/__init__.py
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]
63 changes: 63 additions & 0 deletions amiyabot/adapters/tencent/qqGroup/api.py
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)
163 changes: 163 additions & 0 deletions amiyabot/adapters/tencent/qqGroup/builder.py
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]
26 changes: 26 additions & 0 deletions amiyabot/adapters/tencent/qqGroup/package.py
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)
Loading

0 comments on commit dad89c9

Please sign in to comment.