irispy-client 0.0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iris/__init__.py +6 -0
- iris/bot/__init__.py +115 -0
- iris/bot/_internal/__init__.py +0 -0
- iris/bot/_internal/emitter.py +46 -0
- iris/bot/_internal/iris.py +113 -0
- iris/bot/models.py +382 -0
- iris/cli.py +511 -0
- iris/decorators/__init__.py +49 -0
- iris/kakaolink/KakaoLinkModule.py +450 -0
- iris/kakaolink/__init__.py +61 -0
- iris/util/__init__.py +1 -0
- iris/util/pykv.py +150 -0
- irispy_client-0.0.9.dist-info/METADATA +14 -0
- irispy_client-0.0.9.dist-info/RECORD +17 -0
- irispy_client-0.0.9.dist-info/WHEEL +5 -0
- irispy_client-0.0.9.dist-info/entry_points.txt +2 -0
- irispy_client-0.0.9.dist-info/top_level.txt +1 -0
iris/__init__.py
ADDED
iris/bot/__init__.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
from websockets.sync.client import connect
|
|
7
|
+
from iris.bot._internal.emitter import EventEmitter
|
|
8
|
+
from iris.bot._internal.iris import IrisAPI, IrisRequest
|
|
9
|
+
from iris.bot.models import ChatContext, Message, Room, User
|
|
10
|
+
|
|
11
|
+
class Bot:
|
|
12
|
+
def __init__(self, iris_url: str, *, max_workers=None):
|
|
13
|
+
self.emitter = EventEmitter(max_workers=max_workers)
|
|
14
|
+
|
|
15
|
+
self.iris_url = iris_url.replace(
|
|
16
|
+
"http://",
|
|
17
|
+
"",
|
|
18
|
+
).replace(
|
|
19
|
+
"https://",
|
|
20
|
+
"",
|
|
21
|
+
).replace(
|
|
22
|
+
"ws://",
|
|
23
|
+
"",
|
|
24
|
+
).replace(
|
|
25
|
+
"wss://",
|
|
26
|
+
"",
|
|
27
|
+
)
|
|
28
|
+
if self.iris_url.endswith("/"):
|
|
29
|
+
self.iris_url = self.iris_url[:-1]
|
|
30
|
+
|
|
31
|
+
self.iris_ws_endpoint = f"ws://{self.iris_url}/ws"
|
|
32
|
+
self.api = IrisAPI(f"http://{iris_url}")
|
|
33
|
+
|
|
34
|
+
def __process_chat(self, chat: ChatContext):
|
|
35
|
+
self.emitter.emit("chat", [chat])
|
|
36
|
+
|
|
37
|
+
origin = chat.message.v.get("origin")
|
|
38
|
+
if origin == "MSG":
|
|
39
|
+
self.emitter.emit("message", [chat])
|
|
40
|
+
elif origin == "NEWMEM":
|
|
41
|
+
self.emitter.emit("new_member", [chat])
|
|
42
|
+
elif origin == "DELMEM":
|
|
43
|
+
self.emitter.emit("del_member", [chat])
|
|
44
|
+
|
|
45
|
+
def __process_iris_request(self, req: IrisRequest):
|
|
46
|
+
v = {}
|
|
47
|
+
try:
|
|
48
|
+
v = json.loads(req.raw["v"])
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
room = Room(
|
|
53
|
+
id=int(req.raw["chat_id"]),
|
|
54
|
+
name=req.room,
|
|
55
|
+
api=self.api,
|
|
56
|
+
)
|
|
57
|
+
sender = User(
|
|
58
|
+
id=int(req.raw["user_id"]),
|
|
59
|
+
chat_id=room.id,
|
|
60
|
+
api=self.api,
|
|
61
|
+
name=req.sender,
|
|
62
|
+
bot_id=self.bot_id,
|
|
63
|
+
)
|
|
64
|
+
message = Message(
|
|
65
|
+
id=int(req.raw["id"]),
|
|
66
|
+
type=int(req.raw["type"]),
|
|
67
|
+
msg=req.raw["message"],
|
|
68
|
+
attachment=req.raw["attachment"],
|
|
69
|
+
v=v,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
chat = ChatContext(
|
|
73
|
+
room=room, sender=sender, message=message, raw=req.raw, api=self.api, _bot_id=self.bot_id
|
|
74
|
+
)
|
|
75
|
+
self.__process_chat(chat)
|
|
76
|
+
|
|
77
|
+
def run(self):
|
|
78
|
+
while True:
|
|
79
|
+
try:
|
|
80
|
+
with connect(self.iris_ws_endpoint, close_timeout=0) as ws:
|
|
81
|
+
print("웹소켓에 연결되었습니다")
|
|
82
|
+
self.bot_id = self.api.get_info()["bot_id"]
|
|
83
|
+
while True:
|
|
84
|
+
recv = ws.recv()
|
|
85
|
+
try:
|
|
86
|
+
data: dict = json.loads(recv)
|
|
87
|
+
data["raw"] = data.get("json")
|
|
88
|
+
del data["json"]
|
|
89
|
+
|
|
90
|
+
self.__process_iris_request(IrisRequest(**data))
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(
|
|
93
|
+
"Iris 이벤트를 처리 중 오류가 발생했습니다: {}", e
|
|
94
|
+
)
|
|
95
|
+
except KeyboardInterrupt:
|
|
96
|
+
print("웹소켓 연결을 종료합니다")
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print("웹소켓 연결 오류: {}", e)
|
|
101
|
+
print("3초 후 재연결합니다")
|
|
102
|
+
|
|
103
|
+
time.sleep(3)
|
|
104
|
+
|
|
105
|
+
def on_event(self, name: str):
|
|
106
|
+
def decorator(func: t.Callable):
|
|
107
|
+
self.emitter.register(name, func)
|
|
108
|
+
|
|
109
|
+
def wrapper(*args, **kwargs):
|
|
110
|
+
return func(*args, **kwargs)
|
|
111
|
+
|
|
112
|
+
return wrapper
|
|
113
|
+
|
|
114
|
+
return decorator
|
|
115
|
+
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import traceback
|
|
3
|
+
import typing as t
|
|
4
|
+
from iris.bot.models import ErrorContext
|
|
5
|
+
from iris.util.pykv import PyKV
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EventEmitter:
|
|
10
|
+
def __init__(self, max_workers=None):
|
|
11
|
+
self.ev: dict[str, list[t.Callable]] = {}
|
|
12
|
+
self.pool = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)
|
|
13
|
+
|
|
14
|
+
def register(self, name: str, func: t.Callable):
|
|
15
|
+
name = name.lower()
|
|
16
|
+
|
|
17
|
+
if name not in self.ev:
|
|
18
|
+
self.ev[name] = []
|
|
19
|
+
|
|
20
|
+
self.ev[name].append(func)
|
|
21
|
+
|
|
22
|
+
def emit(self, name: str, args: list[t.Any]):
|
|
23
|
+
name = name.lower()
|
|
24
|
+
|
|
25
|
+
for func in self.ev.get(name, []):
|
|
26
|
+
self.pool.submit(self._handle_event, func, name, args)
|
|
27
|
+
|
|
28
|
+
def _handle_event(self, func, name, args):
|
|
29
|
+
try:
|
|
30
|
+
kv = PyKV()
|
|
31
|
+
func(*args)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
if name == "error":
|
|
34
|
+
print(f"error handler에서 오류가 발생했습니다 ({e})")
|
|
35
|
+
traceback.print_exc()
|
|
36
|
+
return
|
|
37
|
+
else:
|
|
38
|
+
print(f"오류가 발생했습니다 ({e})")
|
|
39
|
+
traceback.print_exc()
|
|
40
|
+
|
|
41
|
+
self.emit(
|
|
42
|
+
"error", [ErrorContext(event=name, func=func, exception=e, args=args)]
|
|
43
|
+
)
|
|
44
|
+
finally:
|
|
45
|
+
kv.close()
|
|
46
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import requests
|
|
3
|
+
import typing as t
|
|
4
|
+
import base64
|
|
5
|
+
from io import BufferedIOBase, BytesIO, BufferedReader
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class IrisRequest:
|
|
10
|
+
msg: str
|
|
11
|
+
room: str
|
|
12
|
+
sender: str
|
|
13
|
+
raw: dict
|
|
14
|
+
|
|
15
|
+
class IrisAPI:
|
|
16
|
+
def __init__(self, iris_endpoint: str):
|
|
17
|
+
self.iris_endpoint = iris_endpoint
|
|
18
|
+
|
|
19
|
+
def __parse(self, res: requests.Response) -> dict:
|
|
20
|
+
try:
|
|
21
|
+
data: dict = res.json()
|
|
22
|
+
except Exception:
|
|
23
|
+
raise Exception(f"Iris 응답 JSON 파싱 오류: {res.text}")
|
|
24
|
+
|
|
25
|
+
if not 200 <= res.status_code <= 299:
|
|
26
|
+
print(f"Iris 오류: {res}")
|
|
27
|
+
raise Exception(f"Iris 오류: {data.get('message', '알 수 없는 오류')}")
|
|
28
|
+
|
|
29
|
+
return data
|
|
30
|
+
|
|
31
|
+
def reply(self, room_id: int, msg: str):
|
|
32
|
+
res = requests.post(
|
|
33
|
+
f"{self.iris_endpoint}/reply",
|
|
34
|
+
json={"type": "text", "room": str(room_id), "data": str(msg)},
|
|
35
|
+
)
|
|
36
|
+
return self.__parse(res)
|
|
37
|
+
|
|
38
|
+
def reply_media(
|
|
39
|
+
self,
|
|
40
|
+
room_id: int,
|
|
41
|
+
files: t.List[BufferedIOBase | bytes | Image.Image | str],
|
|
42
|
+
):
|
|
43
|
+
if type(files) is not list:
|
|
44
|
+
files = [files]
|
|
45
|
+
data = []
|
|
46
|
+
for file in files:
|
|
47
|
+
try:
|
|
48
|
+
if isinstance(file, BufferedIOBase):
|
|
49
|
+
data.append(base64.b64encode(file.read()).decode())
|
|
50
|
+
elif isinstance(file, bytes):
|
|
51
|
+
data.append(base64.b64encode(file).decode())
|
|
52
|
+
elif isinstance(file, Image.Image):
|
|
53
|
+
image_bytes_io = BytesIO()
|
|
54
|
+
img = file.convert("RGBA")
|
|
55
|
+
img.save(image_bytes_io, format="PNG")
|
|
56
|
+
image_bytes_io.seek(0)
|
|
57
|
+
buffered_reader = BufferedReader(image_bytes_io)
|
|
58
|
+
data.append(base64.b64encode(buffered_reader.read()).decode())
|
|
59
|
+
elif isinstance(file, str):
|
|
60
|
+
try:
|
|
61
|
+
if file.startswith("http"):
|
|
62
|
+
res = requests.get(file)
|
|
63
|
+
if res.status_code == 200:
|
|
64
|
+
file = res.content
|
|
65
|
+
else:
|
|
66
|
+
print(f"이미지 다운로드 실패: {res.status_code}")
|
|
67
|
+
else:
|
|
68
|
+
with open(file, "rb") as f:
|
|
69
|
+
file = f.read()
|
|
70
|
+
data.append(base64.b64encode(file).decode())
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(f"이미지 처리 중 오류 발생: {e}")
|
|
73
|
+
else:
|
|
74
|
+
print(f"지원하지 않는 형식입니다: {type(file)}")
|
|
75
|
+
except TypeError as e:
|
|
76
|
+
print(f"이미지 처리 중 오류 발생: {e}")
|
|
77
|
+
continue
|
|
78
|
+
if len(data) > 0:
|
|
79
|
+
res = requests.post(
|
|
80
|
+
f"{self.iris_endpoint}/reply",
|
|
81
|
+
json={
|
|
82
|
+
"type": "image_multiple",
|
|
83
|
+
"room": str(room_id),
|
|
84
|
+
"data": data,
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
return self.__parse(res)
|
|
88
|
+
else:
|
|
89
|
+
print("이미지 전송이 모두 실패하였습니다. 이미지 전송 요청 부분을 확인해주세요.")
|
|
90
|
+
|
|
91
|
+
def decrypt(self, enc: int, b64_ciphertext: str, user_id: int) -> str | None:
|
|
92
|
+
res = requests.post(
|
|
93
|
+
f"{self.iris_endpoint}/decrypt",
|
|
94
|
+
json={"enc": enc, "b64_ciphertext": b64_ciphertext, "user_id": user_id},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
res = self.__parse(res)
|
|
98
|
+
return res.get("plain_text")
|
|
99
|
+
|
|
100
|
+
def query(self, query: str, bind: list[t.Any] | None = None) -> list[dict]:
|
|
101
|
+
res = requests.post(
|
|
102
|
+
f"{self.iris_endpoint}/query", json={"query": query, "bind": bind or []}
|
|
103
|
+
)
|
|
104
|
+
res = self.__parse(res)
|
|
105
|
+
return res.get("data", [])
|
|
106
|
+
|
|
107
|
+
def get_info(self):
|
|
108
|
+
res = requests.get(f"{self.iris_endpoint}/config")
|
|
109
|
+
return self.__parse(res)
|
|
110
|
+
|
|
111
|
+
def get_aot(self):
|
|
112
|
+
res = requests.get(f"{self.iris_endpoint}/aot")
|
|
113
|
+
return self.__parse(res)
|
iris/bot/models.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import typing as t
|
|
3
|
+
from iris.bot._internal.iris import IrisAPI
|
|
4
|
+
import json
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from PIL import Image
|
|
7
|
+
from io import BytesIO, BufferedIOBase
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Message:
|
|
12
|
+
id: int
|
|
13
|
+
type: int
|
|
14
|
+
msg: str
|
|
15
|
+
attachment: str
|
|
16
|
+
v: dict
|
|
17
|
+
|
|
18
|
+
def __post_init__(self):
|
|
19
|
+
self.command, *param = self.msg.split(" ", 1)
|
|
20
|
+
self.has_param = len(param) > 0
|
|
21
|
+
self.param = param[0] if self.has_param else None
|
|
22
|
+
try:
|
|
23
|
+
self.attachment = json.loads(self.attachment)
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
if self.type in [71,27,2,71+16384,27+16384,2+16384]:
|
|
27
|
+
self.image = ChatImage(self)
|
|
28
|
+
else:
|
|
29
|
+
self.image = None
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return f"Message(id={self.id}, type={self.type}, msg={self.msg})"
|
|
33
|
+
|
|
34
|
+
class Room:
|
|
35
|
+
def __init__(self, id: int, name: str, api: IrisAPI):
|
|
36
|
+
self.id = id
|
|
37
|
+
self.name = name
|
|
38
|
+
self._api = api
|
|
39
|
+
|
|
40
|
+
@cached_property
|
|
41
|
+
def type(self) -> t.Optional[str]:
|
|
42
|
+
try:
|
|
43
|
+
results = self._api.query(
|
|
44
|
+
'select type from chat_rooms where id = ?',
|
|
45
|
+
[self.id]
|
|
46
|
+
)
|
|
47
|
+
if results and results[0]:
|
|
48
|
+
fetched_type = results[0].get("type")
|
|
49
|
+
return fetched_type
|
|
50
|
+
else:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
except Exception as e:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
def __repr__(self) -> str:
|
|
57
|
+
return f"Room(id={self.id}, name={self.name})"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class User:
|
|
61
|
+
def __init__(self, id: int, chat_id: int, api: IrisAPI, name: str = None, bot_id: int = None):
|
|
62
|
+
self.id = id
|
|
63
|
+
self._chat_id = chat_id
|
|
64
|
+
self._api = api
|
|
65
|
+
self._name = name
|
|
66
|
+
self._bot_id = bot_id
|
|
67
|
+
self.avatar = Avatar(id, chat_id, api)
|
|
68
|
+
|
|
69
|
+
@cached_property
|
|
70
|
+
def name(self) -> t.Optional[str]:
|
|
71
|
+
try:
|
|
72
|
+
if not self._name:
|
|
73
|
+
if self.id == self._bot_id:
|
|
74
|
+
query = "SELECT T2.nickname FROM chat_rooms AS T1 JOIN db2.open_profile AS T2 ON T1.link_id = T2.link_id WHERE T1.id = ?"
|
|
75
|
+
results = self._api.query(query, [self._chat_id])
|
|
76
|
+
name = results[0].get("nickname")
|
|
77
|
+
elif self.id < 10000000000:
|
|
78
|
+
query = "SELECT name, enc FROM db2.friends WHERE id = ?"
|
|
79
|
+
results = self._api.query(query, [self.id])
|
|
80
|
+
name = results[0].get("name")
|
|
81
|
+
else:
|
|
82
|
+
query = "SELECT nickname,enc FROM db2.open_chat_member WHERE user_id = ?"
|
|
83
|
+
results = self._api.query(query, [self.id])
|
|
84
|
+
name = results[0].get("original_profile_image_url")
|
|
85
|
+
return name
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
return self._name
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
@cached_property
|
|
94
|
+
def type(self) -> t.Optional[str]:
|
|
95
|
+
try:
|
|
96
|
+
if self.id == self._bot_id:
|
|
97
|
+
query = "SELECT T2.link_member_type FROM chat_rooms AS T1 INNER JOIN open_profile AS T2 ON T1.link_id = T2.link_id WHERE T1.id = ?"
|
|
98
|
+
results = self._api.query(query, [self._chat_id])
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
query = "SELECT link_member_type FROM db2.open_chat_member WHERE user_id = ?"
|
|
102
|
+
results = self._api.query(query, [self.id])
|
|
103
|
+
|
|
104
|
+
member_type = int(results[0].get("link_member_type"))
|
|
105
|
+
match member_type:
|
|
106
|
+
case 1:
|
|
107
|
+
return "HOST"
|
|
108
|
+
case 2:
|
|
109
|
+
return "NORMAL"
|
|
110
|
+
case 4:
|
|
111
|
+
return "MANAGER"
|
|
112
|
+
case 8:
|
|
113
|
+
return "BOT"
|
|
114
|
+
case _:
|
|
115
|
+
return "UNKNOWN"
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return "REAL_PROFILE"
|
|
119
|
+
|
|
120
|
+
def __repr__(self) -> str:
|
|
121
|
+
return f"User(name={self.name})"
|
|
122
|
+
|
|
123
|
+
class Avatar:
|
|
124
|
+
def __init__(self, id: int, chat_id: int, api: IrisAPI):
|
|
125
|
+
self._id = id
|
|
126
|
+
self._api = api
|
|
127
|
+
self._chat_id = chat_id
|
|
128
|
+
|
|
129
|
+
@cached_property
|
|
130
|
+
def url(self) -> t.Optional[str]:
|
|
131
|
+
try:
|
|
132
|
+
if self._id < 10000000000:
|
|
133
|
+
query = "SELECT T2.o_profile_image_url FROM chat_rooms AS T1 JOIN db2.open_profile AS T2 ON T1.link_id = T2.link_id WHERE T1.id = ?"
|
|
134
|
+
results = self._api.query(query, [self._chat_id])
|
|
135
|
+
fetched_url = results[0].get("o_profile_image_url")
|
|
136
|
+
else:
|
|
137
|
+
query = "SELECT original_profile_image_url,enc FROM db2.open_chat_member WHERE user_id = ?"
|
|
138
|
+
results = self._api.query(query, [self._id])
|
|
139
|
+
fetched_url = results[0].get("original_profile_image_url")
|
|
140
|
+
return fetched_url
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
@cached_property
|
|
146
|
+
def img(self) -> t.Optional[bytes]:
|
|
147
|
+
avatar_url = self.url
|
|
148
|
+
|
|
149
|
+
if not avatar_url:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
image_data = self.__get_image_from_url(avatar_url)
|
|
154
|
+
return image_data
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(f"아바타 이미지 로딩 실패: {e}")
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def __get_image_from_url(self, url: str) -> Image:
|
|
160
|
+
try:
|
|
161
|
+
img = Image.open(BytesIO(requests.get(url).content))
|
|
162
|
+
img = img.convert("RGBA")
|
|
163
|
+
return img
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print(f"아바타 이미지 로딩 실패: {e}")
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def __repr__(self) -> str:
|
|
169
|
+
return f"Avatar(url={self.url})"
|
|
170
|
+
|
|
171
|
+
class ChatImage:
|
|
172
|
+
def __init__(self, message: Message):
|
|
173
|
+
self.url = self.__get_photo_url(message)
|
|
174
|
+
|
|
175
|
+
@cached_property
|
|
176
|
+
def img(self):
|
|
177
|
+
if not self.url:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
imgs = []
|
|
182
|
+
for url in self.url:
|
|
183
|
+
imgs.append(self.__get_image_from_url(url))
|
|
184
|
+
return imgs
|
|
185
|
+
except Exception as e:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
def __get_photo_url(self, message) -> list:
|
|
189
|
+
try:
|
|
190
|
+
urls = []
|
|
191
|
+
if message.type == 71:
|
|
192
|
+
for item in message.attachment["C"]["THL"]:
|
|
193
|
+
urls.append(item["TH"]["THU"])
|
|
194
|
+
elif message.type == 27:
|
|
195
|
+
for item in message.attachment["imageUrls"]:
|
|
196
|
+
urls.append(item)
|
|
197
|
+
else:
|
|
198
|
+
urls.append(message.attachment["url"])
|
|
199
|
+
return urls
|
|
200
|
+
except Exception as e:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def __get_image_from_url(self, url: str) -> Image:
|
|
204
|
+
try:
|
|
205
|
+
img = Image.open(BytesIO(requests.get(url).content))
|
|
206
|
+
img = img.convert("RGBA")
|
|
207
|
+
return img
|
|
208
|
+
except Exception as e:
|
|
209
|
+
print(f"이미지 로딩 실패: {e}")
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
def __repr__(self) -> str:
|
|
213
|
+
return f"ChatImage(url={self.url})"
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class ChatContext:
|
|
217
|
+
room: Room
|
|
218
|
+
sender: User
|
|
219
|
+
message: Message
|
|
220
|
+
raw: dict
|
|
221
|
+
api: IrisAPI
|
|
222
|
+
_bot_id: int = None
|
|
223
|
+
|
|
224
|
+
def __post_init__(self):
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
def reply(self, message: str, room_id: int = None):
|
|
228
|
+
if room_id is None:
|
|
229
|
+
room_id = self.room.id
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
self.api.reply(room_id, message)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
print(f"reply 오류: {e}")
|
|
235
|
+
|
|
236
|
+
def reply_media(
|
|
237
|
+
self,
|
|
238
|
+
files: t.List[BufferedIOBase | bytes | Image.Image | str],
|
|
239
|
+
room_id: int = None,
|
|
240
|
+
):
|
|
241
|
+
if room_id is None:
|
|
242
|
+
room_id = self.room.id
|
|
243
|
+
|
|
244
|
+
self.api.reply_media(room_id, files)
|
|
245
|
+
|
|
246
|
+
def get_source(self):
|
|
247
|
+
source_record = self.__get_reply_chat(self.message)
|
|
248
|
+
if source_record:
|
|
249
|
+
source_chat = self.__make_chat(self, source_record)
|
|
250
|
+
return source_chat
|
|
251
|
+
else:
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
def get_next_chat(self, n: int = 1):
|
|
255
|
+
next_record = self.__get_next_record(self.message.id, n)
|
|
256
|
+
if next_record:
|
|
257
|
+
next_chat = self.__make_chat(self, next_record)
|
|
258
|
+
return next_chat
|
|
259
|
+
else:
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
def get_previous_chat(self, n: int = 1):
|
|
263
|
+
previous_record = self.__get_previous_record(self.message.id, n)
|
|
264
|
+
if previous_record:
|
|
265
|
+
previous_chat = self.__make_chat(self, previous_record)
|
|
266
|
+
return previous_chat
|
|
267
|
+
else:
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
def __get_reply_chat(self, message: Message):
|
|
271
|
+
try:
|
|
272
|
+
src_log_id = message.attachment['src_logId']
|
|
273
|
+
query = "select * from chat_logs where id = ?"
|
|
274
|
+
src_record = self.api.query(query,[src_log_id])
|
|
275
|
+
return src_record[0]
|
|
276
|
+
except Exception as e:
|
|
277
|
+
print(e)
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def __get_previous_record(self, log_id, n: int = 1):
|
|
281
|
+
if n < 0:
|
|
282
|
+
raise ValueError("n must be greater than 0")
|
|
283
|
+
|
|
284
|
+
query = """
|
|
285
|
+
WITH RECURSIVE ChatHistory AS (
|
|
286
|
+
SELECT *
|
|
287
|
+
FROM chat_logs
|
|
288
|
+
WHERE id = ?
|
|
289
|
+
UNION ALL
|
|
290
|
+
SELECT c.*
|
|
291
|
+
FROM chat_logs c
|
|
292
|
+
JOIN ChatHistory h ON c.id = h.prev_id
|
|
293
|
+
)
|
|
294
|
+
SELECT *
|
|
295
|
+
FROM ChatHistory
|
|
296
|
+
LIMIT 1 OFFSET ?;
|
|
297
|
+
"""
|
|
298
|
+
record = self.api.query(query, [log_id, n])
|
|
299
|
+
return record[0] if record else None
|
|
300
|
+
|
|
301
|
+
def __get_next_record(self, log_id, n: int = 1):
|
|
302
|
+
n = n - 1
|
|
303
|
+
if n < -1:
|
|
304
|
+
raise ValueError("n must be greater than 0")
|
|
305
|
+
query = """
|
|
306
|
+
WITH RECURSIVE ChatHistory AS (
|
|
307
|
+
SELECT
|
|
308
|
+
*,
|
|
309
|
+
0 AS depth
|
|
310
|
+
FROM
|
|
311
|
+
chat_logs
|
|
312
|
+
WHERE
|
|
313
|
+
id = ?
|
|
314
|
+
|
|
315
|
+
UNION ALL
|
|
316
|
+
|
|
317
|
+
SELECT
|
|
318
|
+
c.*,
|
|
319
|
+
h.depth + 1
|
|
320
|
+
FROM
|
|
321
|
+
chat_logs c
|
|
322
|
+
JOIN ChatHistory h ON c.prev_id = h.id
|
|
323
|
+
WHERE
|
|
324
|
+
h.depth < 100
|
|
325
|
+
AND c.prev_id IS NOT NULL
|
|
326
|
+
AND h.id IS NOT NULL
|
|
327
|
+
AND c.id IS NOT NULL
|
|
328
|
+
)
|
|
329
|
+
SELECT *
|
|
330
|
+
FROM ChatHistory
|
|
331
|
+
WHERE depth = ? + 1
|
|
332
|
+
LIMIT 1;
|
|
333
|
+
"""
|
|
334
|
+
record = self.api.query(query, [log_id, n])
|
|
335
|
+
return record[0] if record else None
|
|
336
|
+
|
|
337
|
+
def __make_chat(self, chat, record):
|
|
338
|
+
v = {}
|
|
339
|
+
try:
|
|
340
|
+
v = json.loads(record["v"])
|
|
341
|
+
except Exception:
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
room = Room(
|
|
345
|
+
id=int(record["chat_id"]),
|
|
346
|
+
name=chat.room.name,
|
|
347
|
+
api=self.api,
|
|
348
|
+
)
|
|
349
|
+
sender = User(
|
|
350
|
+
id=int(record["user_id"]),
|
|
351
|
+
chat_id=chat.room.id,
|
|
352
|
+
api=self.api,
|
|
353
|
+
name=self.__get_name_of_user_id(int(record["user_id"])),
|
|
354
|
+
bot_id=self._bot_id,
|
|
355
|
+
)
|
|
356
|
+
message = Message(
|
|
357
|
+
id=int(record["id"]),
|
|
358
|
+
type=int(record["type"]),
|
|
359
|
+
msg=record["message"],
|
|
360
|
+
attachment=record["attachment"],
|
|
361
|
+
v=v,
|
|
362
|
+
)
|
|
363
|
+
new_chat = ChatContext(
|
|
364
|
+
room=room, sender=sender, message=message, raw=record, api=self.api, _bot_id=self._bot_id
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
return new_chat
|
|
368
|
+
|
|
369
|
+
def __get_name_of_user_id(self, user_id: int):
|
|
370
|
+
query = "WITH info AS (SELECT ? AS user_id) SELECT COALESCE(open_chat_member.nickname, friends.name) AS name, COALESCE(open_chat_member.enc, friends.enc) AS enc FROM info LEFT JOIN db2.open_chat_member ON open_chat_member.user_id = info.user_id LEFT JOIN db2.friends ON friends.id = info.user_id;"
|
|
371
|
+
result = self.api.query(query,[user_id])
|
|
372
|
+
if len(result) == 0:
|
|
373
|
+
return None
|
|
374
|
+
return result[0]['name']
|
|
375
|
+
|
|
376
|
+
@dataclass
|
|
377
|
+
class ErrorContext:
|
|
378
|
+
event: str
|
|
379
|
+
func: t.Callable
|
|
380
|
+
exception: Exception
|
|
381
|
+
args: list[t.Any]
|
|
382
|
+
|