Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: 'Test'

on:
pull_request:
push:
branches:
- master

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: actions/checkout@v4

- name: "Setup Python"
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: "Setup Poetry"
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true

- name: "Install dependencies"
run: |
poetry install --only main,test

- name: "Run tests"
run: |
poetry run pytest
15 changes: 13 additions & 2 deletions actions/v11/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import utils.translator as translator
import utils.message.v11.parser as parser
import utils.return_object as return_object
from utils.cache import get_cache_dir
import utils.forward_merge as forward_merge
from utils.config import config
from utils.client import client
import utils.message.v11.parser as parser
Expand Down Expand Up @@ -71,9 +73,9 @@ async def delete_msg(message_id: int) -> dict:


def clean_node_cache() -> None:
for file in os.listdir(".cache"):
for file in os.listdir(get_cache_dir()):
if file.startswith("node."):
os.remove(os.path.join(".cache", file))
os.remove(os.path.join(get_cache_dir(), file))


@register_action("v11")
Expand Down Expand Up @@ -352,6 +354,15 @@ async def send_private_forward_msg(user_id: int, messages: list) -> dict:
)


@register_action("v11")
async def get_forward_msg(message_id: str) -> dict:
"""获取合并转发消息(接收方向:转发消息自动合并后由框架按 id 取回)"""
nodes = await forward_merge.get_forward(message_id)
if nodes is None:
return return_object.get(400, f"合并转发消息 {message_id} 不存在")
return return_object.get(0, message_id=message_id, message=nodes)


async def _restart() -> None:
script = sys.argv[0]
args = sys.argv[1:]
Expand Down
4 changes: 2 additions & 2 deletions actions/v11/get_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import httpx
from ..v12.file import get_file_name_by_id
from utils import return_object
from pathlib import Path
from utils.cache import get_file_path


@register_action("v11")
async def get_image(file: str) -> dict:
file_name = await get_file_name_by_id(file.split("_")[0])
if not file_name:
return return_object.get(31001, f"文件 {file} 不存在")
return return_object.get(0, file=Path(".cache/file").joinpath(file_name).as_posix())
return return_object.get(0, file=get_file_path(file_name))
2 changes: 1 addition & 1 deletion actions/v12/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async def send_message(
if not (channel := client.get_channel(int(_channel_id))):
logger.warning(f"频道 {group_id} 不存在")
return return_object.get(35001, "频道(群号)不存在")
parsed_message = await parser.parse_message(message)
parsed_message = await parser.parse_message(message, channel.id)
if _channel_id not in commands.deferred_sessions:
try:
msg = await channel.send(**parsed_message) # type: ignore
Expand Down
56 changes: 28 additions & 28 deletions actions/v12/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@
import hashlib
import json
import httpx
import time
import utils.return_object as return_object
from utils.cache import files_dir, file_list_path, cached_url_path, get_file_path

try:
os.makedirs(".cache/files")
files_dir()
except OSError:
pass

try:
json.load(open(".cache/file_list.json", "r", encoding="utf-8"))
json.load(open(file_list_path(), "r", encoding="utf-8"))
except Exception:
json.dump({}, open(".cache/file_list.json", "w", encoding="utf-8"))
json.dump({}, open(file_list_path(), "w", encoding="utf-8"))
try:
json.load(open(".cache/cached_url.json", "r", encoding="utf-8"))
json.load(open(cached_url_path(), "r", encoding="utf-8"))
except Exception:
json.dump({}, open(".cache/cached_url.json", "w", encoding="utf-8"))
json.dump({}, open(cached_url_path(), "w", encoding="utf-8"))

logger = get_logger()

Expand All @@ -36,10 +38,12 @@ def verify_sha256(content: bytes, sha256: str | None) -> bool:


def create_url_cache(name: str, url: str) -> str:
with open(".cache/cached_url.json", "r", encoding="utf-8") as f:
with open(cached_url_path(), "r", encoding="utf-8") as f:
cache = json.load(f)
cache[file_id := create_file_id()] = {"name": name, "url": url}
with open(".cache/cached_url.json", "w", encoding="utf-8") as f:
cache[file_id := create_file_id()] = {
"name": name, "url": url, "time": int(time.time())
}
with open(cached_url_path(), "w", encoding="utf-8") as f:
json.dump(cache, f)
return file_id

Expand All @@ -56,7 +60,7 @@ async def upload_file_from_url(
async with httpx.AsyncClient(proxies=proxy) as client:
response = await client.get(url, headers=headers)
if response.status_code == 200 and verify_sha256(response.content, sha256):
with open(f".cache/files/{name}", "wb") as f:
with open(get_file_path(name), "wb") as f:
f.write(response.content)
return True
logger.warning(
Expand All @@ -75,7 +79,7 @@ async def upload_file_from_url(

def upload_file_from_data(name: str, data: str) -> tuple[bool, str]:
try:
with open(f".cache/files/{name}", "wb") as f:
with open(get_file_path(name), "wb") as f:
f.write(base64.b64decode(data))
return True, ""
except Exception as e:
Expand All @@ -85,29 +89,29 @@ def upload_file_from_data(name: str, data: str) -> tuple[bool, str]:

def create_file_id() -> str:
file_id = str(uuid.uuid1())
with open(".cache/file_list.json", "r", encoding="utf-8") as f:
with open(file_list_path(), "r", encoding="utf-8") as f:
if file_id in json.load(f).keys():
return create_file_id()
with open(".cache/cached_url.json", "r", encoding="utf-8") as f:
with open(cached_url_path(), "r", encoding="utf-8") as f:
if file_id in json.load(f).keys():
return create_file_id()
return file_id


def register_saved_file(name: str, _file_id: str | None = None) -> str:
file_id = _file_id or create_file_id()
with open(".cache/file_list.json", "r", encoding="utf-8") as f:
with open(file_list_path(), "r", encoding="utf-8") as f:
file_list = json.load(f)
file_list[file_id] = name
with open(".cache/file_list.json", "w", encoding="utf-8") as f:
with open(file_list_path(), "w", encoding="utf-8") as f:
json.dump(file_list, f, ensure_ascii=False, indent=4)
return file_id


def upload_file_from_path(name: str, path: str) -> tuple[bool, str]:
try:
with open(path, "rb") as from_f:
with open(f".cache/files/{name}", "wb") as to_f:
with open(get_file_path(name), "wb") as to_f:
to_f.write(from_f.read())
return True, ""
except Exception as e:
Expand Down Expand Up @@ -177,7 +181,7 @@ async def upload_file_fragmented(
case "finish":
type_checker.check_arguments(file_id, offset, sha256)
file_name = uploading_files[file_id]["name"]
with open(f".cache/files/{file_name}", "wb") as f:
with open(get_file_path(file_name), "wb") as f:
f.write(bytes(uploading_files.pop(file_id)["content"]))
# TODO sha256 校验
return return_object.get(file_id=register_saved_file(file_name))
Expand Down Expand Up @@ -234,11 +238,11 @@ async def get_file_name_by_id(file_id: str) -> str | None:
"""
根据文件 ID 获取文件名
"""
with open(f".cache/file_list.json", "r", encoding="utf-8") as f:
with open(ffile_list_path(), "r", encoding="utf-8") as f:
file_list = json.load(f)
if _id := file_list.get(file_id):
return _id
with open(".cache/cached_url.json", "r", encoding="utf-8") as f:
with open(cached_url_path(), "r", encoding="utf-8") as f:
cached_url_list = json.load(f)
if cache_data := cached_url_list.get(file_id):
return await get_file_name_by_id(
Expand All @@ -249,9 +253,9 @@ async def get_file_name_by_id(file_id: str) -> str | None:


async def clean_files() -> None:
with open(".cache/file_list.json", "r", encoding="utf-8") as f:
with open(file_list_path(), "r", encoding="utf-8") as f:
file_list = json.load(f)
with open(".cache/cached_url.json", "r", encoding="utf-8") as f:
with open(cached_url_path(), "r", encoding="utf-8") as f:
cached_url_list = json.load(f)
for file_id in list(file_list.keys()):
if not os.path.exists(get_file_path(file_list[file_id])):
Expand All @@ -272,16 +276,12 @@ async def clean_files() -> None:
cached_url_list.pop(file_id)
logger.debug(file_list)
logger.debug(cached_url_list)
with open(".cache/file_list.json", "w", encoding="utf-8") as f:
with open(file_list_path(), "w", encoding="utf-8") as f:
json.dump(file_list, f, ensure_ascii=False, indent=4)
with open(".cache/cached_url.json", "w", encoding="utf-8") as f:
with open(cached_url_path(), "w", encoding="utf-8") as f:
json.dump(cached_url_list, f, ensure_ascii=False, indent=4)


def get_file_path(file_name: str) -> str:
return os.path.abspath(f".cache/files/{file_name}")


@register_action()
async def get_file(file_id: str, type: str) -> dict:
"""
Expand All @@ -297,12 +297,12 @@ async def get_file(file_id: str, type: str) -> dict:

case "path":
return return_object.get(
0, name=file, path=os.path.abspath(f".cache/files/{file}")
0, name=file, path=get_file_path(file)
)
# TODO 返回 sha256

case "data":
with open(f".cache/files/{file}", "rb") as f:
with open(get_file_path(file), "rb") as f:
return return_object.get(
0, name=file, data=base64.b64encode(f.read()).decode("utf-8")
)
Expand Down
47 changes: 44 additions & 3 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,57 @@ OneDisc 高级设置(无特殊需要不建议更改)

| 类型 | 必须 | 默认值 |
|:----------:|:----:|:----------------------:|
| 字符串 | 否 | `sqlite+aiosqlite:///:memory:` |
| 字符串 | 否 | `sqlite+aiosqlite:///缓存目录/onedisc.db` |

OneDisc 缓存消息使用的数据库地址

参考 [Engine Configuration — SQLAlchemy 2.0 Documentation](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)

不支持自动创建数据库
未配置(或为 `null`)时,自动使用缓存目录(`cache_dir`)下的 `onedisc.db`,目录不存在会自动创建

> 目前可执行版只支持 SQLite3,源码版使用其他数据库需要手动安装依赖

### 缓存目录(`cache_dir`)

| 类型 | 必须 | 默认值 |
|:----------:|:----:|:----------------------:|
| 字符串 | 否 | `.cache` |

OneDisc 缓存文件、缓存索引(`file_list.json` / `cached_url.json`)与默认数据库的存放目录

### 缓存过期清理(`cache`)

| 类型 | 必须 | 默认值 |
|:----------:|:----:|:----------------------:|
| 对象 | 否 | `{}`(全部不过期) |

配置各类缓存的过期时间(秒)与自动清理间隔:

| 字段 | 说明 | 默认值 |
|:------------------:|:-----------------------------------------:|:-----------:|
| `files_ttl` | 文件缓存(`files/` 目录与 `node.*` 节点)过期秒数 | `0`(不过期) |
| `url_cache_ttl` | URL 缓存索引(`cached_url.json`)过期秒数 | `0`(不过期) |
| `db_ttl` | 本地消息数据库记录过期秒数 | `0`(不过期) |
| `cleanup_interval` | 自动清理检查间隔秒数 | `3600` |

所有 TTL 为 `0`(默认)时表示永不过期,不会自动清理任何缓存;只有配置了 TTL,程序才会在每个清理周期删除过期条目

### 合并转发消息(`merge_forward`)

| 类型 | 必须 | 默认值 |
|:----------:|:----:|:----------------------:|
| 布尔 | 否 | `true` |

接收方向(Discord → OneBot):将同一频道同一发送者在 `merge_forward_interval` 内连续发送的「转发」消息自动合并为一条 OneBot V11 合并转发(forward)消息上报,框架收到后可通过 `get_forward_msg` 动作按 id 取回完整消息列表

### 合并转发窗口时长(`merge_forward_interval`)

| 类型 | 必须 | 默认值 |
|:----------:|:----:|:----------------------:|
| 整数(毫秒)| 否 | `500` |

合并转发的收集窗口时长,窗口内到达的满足条件的转发消息会被吸收进同一条合并转发,不再单独上报

### 使用静态表情(`use_static_face`)

| 类型 | 必须 | 默认值 |
Expand Down Expand Up @@ -271,7 +312,7 @@ OneBot V11 中,私聊消息事件(`message.private`)的 `sub_type` 字段
|:---------:|:----:|:----------------:|
| 布尔 | 否 | `false` |

此项为 `true` 时,当同一文件同时存在于缓存 URL 索引和本地储存库(`.cache/files`)时,优先保留缓存(删除本地储存文件)
此项为 `true` 时,当同一文件同时存在于缓存 URL 索引和本地储存库(`cache_dir` 下的 `files/` 目录)时,优先保留缓存(删除本地储存文件)

此项为 `false` 时,将删除缓存库的索引

Expand Down
2 changes: 1 addition & 1 deletion docs/diff-v11.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

| 接口名称 | 终结点 | 说明 |
|------------------|---------------------------|--------------------------|
| 获取合并转发消息 | `get_forward_msg` | Discord 不支持相关功能 |
| 获取合并转发消息 | `get_forward_msg` | 仅支持查询 OneDisc 自动合并转发产生的消息(id 格式为「频道id_消息id」,见 `merge_forward` 配置项),其他 id 返回错误;Discord 本身没有合并转发概念 |
| 发送好友赞 | `send_like` | Discord 不支持相关功能 |
| 群组匿名用户禁言 | `set_group_anonymous_ban` | Discord 不支持相关功能 |
| 群组全员禁言 | `set_group_whole_ban` | Discord 不支持相关功能 |
Expand Down
Loading
Loading