bemyai 0.1.8__tar.gz
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.
- bemyai-0.1.8/PKG-INFO +147 -0
- bemyai-0.1.8/README.md +126 -0
- bemyai-0.1.8/bemyai/__init__.py +534 -0
- bemyai-0.1.8/bemyai/__main__.py +169 -0
- bemyai-0.1.8/bemyai/langs.py +219 -0
- bemyai-0.1.8/bemyai/schema.py +137 -0
- bemyai-0.1.8/pyproject.toml +27 -0
bemyai-0.1.8/PKG-INFO
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: bemyai
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: Be My AI API
|
|
5
|
+
Author: Alexey
|
|
6
|
+
Author-email: aleks-samos@yandex.ru
|
|
7
|
+
Requires-Python: >3.8
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: aiofiles (>=23.2.1,<24.0.0)
|
|
15
|
+
Requires-Dist: aiohttp (>=3.9.1,<4.0.0)
|
|
16
|
+
Requires-Dist: loguru (>=0.7.2,<0.8.0)
|
|
17
|
+
Requires-Dist: pillow (>=10.2.0,<11.0.0)
|
|
18
|
+
Requires-Dist: pydantic (>=2.5.3,<3.0.0)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
### Be My AI
|
|
22
|
+
|
|
23
|
+
#### Console
|
|
24
|
+
```bash
|
|
25
|
+
bm login -l ru "your_email@domain.tld "1yourpassword3"
|
|
26
|
+
bm -l ru recognize "path/to/photo.jpg"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
##### Example
|
|
30
|
+
```
|
|
31
|
+
# building
|
|
32
|
+
E:\bemyai>poetry build
|
|
33
|
+
|
|
34
|
+
# installing
|
|
35
|
+
E:\bemyai>pip install .\dist\bemyai-0.1.8-py3-none-any.whl
|
|
36
|
+
|
|
37
|
+
# I'm already logged in
|
|
38
|
+
|
|
39
|
+
# Recognizing...
|
|
40
|
+
E:\bemyai>bm -l ru recognize E:\aribook1_b.jpg
|
|
41
|
+
На фотографии изображена обложка книги с изображением Арианы Гранде. В верхней части обложки написано "100% НЕОФИЦИАЛЬНО" на фоне розового прямоугольника. Ниже следует текст:
|
|
42
|
+
|
|
43
|
+
Ариана — не просто очередная эстрадная принцесса, она —
|
|
44
|
+
одна из крупнейших мировых звезд
|
|
45
|
+
и самая популярная женщина в Инстаграме.
|
|
46
|
+
|
|
47
|
+
Далее идет желтый прямоугольник с текстом:
|
|
48
|
+
|
|
49
|
+
Любой альбом и любой сингл всегда занимает верхние строки в чартах,
|
|
50
|
+
Ариана является иконой стиля, достойной защитницей прав женщин,
|
|
51
|
+
добровольцем, актрисой и выжившей жертвой теракта.
|
|
52
|
+
В своей жизни она ставит на первое место семью, включая
|
|
53
|
+
семерых спасенных ею собак!
|
|
54
|
+
|
|
55
|
+
С помощью четырех октав Ариана, вероятно, может считаться пушкой
|
|
56
|
+
в мире поп-музыке.
|
|
57
|
+
И если кто-то думает
|
|
58
|
+
что способен приблизиться
|
|
59
|
+
к ее коронованной особе,
|
|
60
|
+
то пусть еще раз послушает
|
|
61
|
+
песню Арианы Гранде
|
|
62
|
+
Thank You, Next!
|
|
63
|
+
|
|
64
|
+
В нижней части обложки на розовом фоне написано "100% ИДОЛ". Также присутствует штрих-код с ISBN номером 978-5-04-114282-3 и ссылка на веб-сайт www.vk.com/eskmo_kids.
|
|
65
|
+
|
|
66
|
+
# You can ask GPT about this image in the chat
|
|
67
|
+
# Type Q for exit
|
|
68
|
+
# Or pass `-ni` to return the response and exit
|
|
69
|
+
# in this case, you can ask a question with an ask command
|
|
70
|
+
# e.g.
|
|
71
|
+
# bm ask "Your question"
|
|
72
|
+
E:\bemyai>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Python
|
|
76
|
+
```python
|
|
77
|
+
import sys
|
|
78
|
+
import asyncio
|
|
79
|
+
from bemyai import BeMyAI
|
|
80
|
+
from loguru import logger
|
|
81
|
+
logger.remove()
|
|
82
|
+
logger.add(sys.stderr, format="{message}", level="INFO")
|
|
83
|
+
|
|
84
|
+
async def main():
|
|
85
|
+
# you can specify another language,
|
|
86
|
+
# for example, Russian
|
|
87
|
+
|
|
88
|
+
# get token
|
|
89
|
+
bm = BeMyAI(response_language="en")
|
|
90
|
+
result = await bm.login(
|
|
91
|
+
"test@example.com",
|
|
92
|
+
"yourpassword"
|
|
93
|
+
)
|
|
94
|
+
# and save result.token for future requests
|
|
95
|
+
|
|
96
|
+
# authorization by token
|
|
97
|
+
bm = BeMyAI("your_token", response_language="en")
|
|
98
|
+
|
|
99
|
+
# recognize photo
|
|
100
|
+
sid, chat_id = await bm.take_photo("pic.jpg")
|
|
101
|
+
for i in range(2):
|
|
102
|
+
async for bm_response in bm.receive_messages(sid):
|
|
103
|
+
message = bm_response
|
|
104
|
+
if message.user:
|
|
105
|
+
continue
|
|
106
|
+
print( message.data )
|
|
107
|
+
if i == 0:
|
|
108
|
+
# We ask a question
|
|
109
|
+
sid, chat_id, _message = await bm.send_text_message(
|
|
110
|
+
chat_id,
|
|
111
|
+
"Describe it in more detail"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
asyncio.run(main())
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
##### Example output
|
|
120
|
+
|
|
121
|
+
I saved the python example,
|
|
122
|
+
specified my token and the Russian language,
|
|
123
|
+
specified the path to the image and got this result:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
dl folder already exists
|
|
127
|
+
get app user config from internet
|
|
128
|
+
recognizing new photo: JPEG, 1215x2160, RGB
|
|
129
|
+
create new chat
|
|
130
|
+
resizing image
|
|
131
|
+
image processed: jpeg, 1125x2000
|
|
132
|
+
requested upload image config
|
|
133
|
+
get app user config from cache
|
|
134
|
+
Starting upload image to Amazon
|
|
135
|
+
Uploaded successfully
|
|
136
|
+
removeing processed image
|
|
137
|
+
upload image finished
|
|
138
|
+
Got new message
|
|
139
|
+
Got new message
|
|
140
|
+
На фотографии изображена книга с названием "Ариана Гранде. Главная книга фаната". На обложке множество фотографий Арианы Гранде в разных образах. В верхнем левом углу написано "100% неофициально". В нижнем правом углу обложки есть маркировка "18+".
|
|
141
|
+
Got new message
|
|
142
|
+
Got new message
|
|
143
|
+
На обложке книги изображено семь различных фотографий певицы Арианы Гранде. Она показана в разных нарядах и прическах, в том числе с её знаменитым высоким хвостом. На одной из фотографий она в большом белом банте на голове. На другой - в черном топе и с кепкой. Есть изображение, где она поет в микрофон, закрыв глаза и подняв голову вверх. Цвета обложки - розовый, белый и черный. На обложке также присутствуют розовые и белые геометрические элементы, а также надписи розового цвета.
|
|
144
|
+
|
|
145
|
+
E:\bemyai>
|
|
146
|
+
```
|
|
147
|
+
|
bemyai-0.1.8/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
### Be My AI
|
|
2
|
+
|
|
3
|
+
#### Console
|
|
4
|
+
```bash
|
|
5
|
+
bm login -l ru "your_email@domain.tld "1yourpassword3"
|
|
6
|
+
bm -l ru recognize "path/to/photo.jpg"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
##### Example
|
|
10
|
+
```
|
|
11
|
+
# building
|
|
12
|
+
E:\bemyai>poetry build
|
|
13
|
+
|
|
14
|
+
# installing
|
|
15
|
+
E:\bemyai>pip install .\dist\bemyai-0.1.8-py3-none-any.whl
|
|
16
|
+
|
|
17
|
+
# I'm already logged in
|
|
18
|
+
|
|
19
|
+
# Recognizing...
|
|
20
|
+
E:\bemyai>bm -l ru recognize E:\aribook1_b.jpg
|
|
21
|
+
На фотографии изображена обложка книги с изображением Арианы Гранде. В верхней части обложки написано "100% НЕОФИЦИАЛЬНО" на фоне розового прямоугольника. Ниже следует текст:
|
|
22
|
+
|
|
23
|
+
Ариана — не просто очередная эстрадная принцесса, она —
|
|
24
|
+
одна из крупнейших мировых звезд
|
|
25
|
+
и самая популярная женщина в Инстаграме.
|
|
26
|
+
|
|
27
|
+
Далее идет желтый прямоугольник с текстом:
|
|
28
|
+
|
|
29
|
+
Любой альбом и любой сингл всегда занимает верхние строки в чартах,
|
|
30
|
+
Ариана является иконой стиля, достойной защитницей прав женщин,
|
|
31
|
+
добровольцем, актрисой и выжившей жертвой теракта.
|
|
32
|
+
В своей жизни она ставит на первое место семью, включая
|
|
33
|
+
семерых спасенных ею собак!
|
|
34
|
+
|
|
35
|
+
С помощью четырех октав Ариана, вероятно, может считаться пушкой
|
|
36
|
+
в мире поп-музыке.
|
|
37
|
+
И если кто-то думает
|
|
38
|
+
что способен приблизиться
|
|
39
|
+
к ее коронованной особе,
|
|
40
|
+
то пусть еще раз послушает
|
|
41
|
+
песню Арианы Гранде
|
|
42
|
+
Thank You, Next!
|
|
43
|
+
|
|
44
|
+
В нижней части обложки на розовом фоне написано "100% ИДОЛ". Также присутствует штрих-код с ISBN номером 978-5-04-114282-3 и ссылка на веб-сайт www.vk.com/eskmo_kids.
|
|
45
|
+
|
|
46
|
+
# You can ask GPT about this image in the chat
|
|
47
|
+
# Type Q for exit
|
|
48
|
+
# Or pass `-ni` to return the response and exit
|
|
49
|
+
# in this case, you can ask a question with an ask command
|
|
50
|
+
# e.g.
|
|
51
|
+
# bm ask "Your question"
|
|
52
|
+
E:\bemyai>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Python
|
|
56
|
+
```python
|
|
57
|
+
import sys
|
|
58
|
+
import asyncio
|
|
59
|
+
from bemyai import BeMyAI
|
|
60
|
+
from loguru import logger
|
|
61
|
+
logger.remove()
|
|
62
|
+
logger.add(sys.stderr, format="{message}", level="INFO")
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
# you can specify another language,
|
|
66
|
+
# for example, Russian
|
|
67
|
+
|
|
68
|
+
# get token
|
|
69
|
+
bm = BeMyAI(response_language="en")
|
|
70
|
+
result = await bm.login(
|
|
71
|
+
"test@example.com",
|
|
72
|
+
"yourpassword"
|
|
73
|
+
)
|
|
74
|
+
# and save result.token for future requests
|
|
75
|
+
|
|
76
|
+
# authorization by token
|
|
77
|
+
bm = BeMyAI("your_token", response_language="en")
|
|
78
|
+
|
|
79
|
+
# recognize photo
|
|
80
|
+
sid, chat_id = await bm.take_photo("pic.jpg")
|
|
81
|
+
for i in range(2):
|
|
82
|
+
async for bm_response in bm.receive_messages(sid):
|
|
83
|
+
message = bm_response
|
|
84
|
+
if message.user:
|
|
85
|
+
continue
|
|
86
|
+
print( message.data )
|
|
87
|
+
if i == 0:
|
|
88
|
+
# We ask a question
|
|
89
|
+
sid, chat_id, _message = await bm.send_text_message(
|
|
90
|
+
chat_id,
|
|
91
|
+
"Describe it in more detail"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
asyncio.run(main())
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
##### Example output
|
|
100
|
+
|
|
101
|
+
I saved the python example,
|
|
102
|
+
specified my token and the Russian language,
|
|
103
|
+
specified the path to the image and got this result:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
dl folder already exists
|
|
107
|
+
get app user config from internet
|
|
108
|
+
recognizing new photo: JPEG, 1215x2160, RGB
|
|
109
|
+
create new chat
|
|
110
|
+
resizing image
|
|
111
|
+
image processed: jpeg, 1125x2000
|
|
112
|
+
requested upload image config
|
|
113
|
+
get app user config from cache
|
|
114
|
+
Starting upload image to Amazon
|
|
115
|
+
Uploaded successfully
|
|
116
|
+
removeing processed image
|
|
117
|
+
upload image finished
|
|
118
|
+
Got new message
|
|
119
|
+
Got new message
|
|
120
|
+
На фотографии изображена книга с названием "Ариана Гранде. Главная книга фаната". На обложке множество фотографий Арианы Гранде в разных образах. В верхнем левом углу написано "100% неофициально". В нижнем правом углу обложки есть маркировка "18+".
|
|
121
|
+
Got new message
|
|
122
|
+
Got new message
|
|
123
|
+
На обложке книги изображено семь различных фотографий певицы Арианы Гранде. Она показана в разных нарядах и прическах, в том числе с её знаменитым высоким хвостом. На одной из фотографий она в большом белом банте на голове. На другой - в черном топе и с кепкой. Есть изображение, где она поет в микрофон, закрыв глаза и подняв голову вверх. Цвета обложки - розовый, белый и черный. На обложке также присутствуют розовые и белые геометрические элементы, а также надписи розового цвета.
|
|
124
|
+
|
|
125
|
+
E:\bemyai>
|
|
126
|
+
```
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""
|
|
2
|
+
unofficial API BeMyAI from Android app
|
|
3
|
+
"""
|
|
4
|
+
from .langs import LANGS, LOCALES
|
|
5
|
+
from .schema import (
|
|
6
|
+
LoginResponseModel,
|
|
7
|
+
AppConfigUserModel,
|
|
8
|
+
ChatModel,
|
|
9
|
+
ChatConfigModel,
|
|
10
|
+
ChatUploadImageConfigModel,
|
|
11
|
+
ChatMessagesModel,
|
|
12
|
+
)
|
|
13
|
+
import os
|
|
14
|
+
import asyncio
|
|
15
|
+
from functools import partial
|
|
16
|
+
import json
|
|
17
|
+
import shutil
|
|
18
|
+
from uuid import uuid4
|
|
19
|
+
from datetime import datetime, timezone, timedelta
|
|
20
|
+
import os.path
|
|
21
|
+
from math import floor
|
|
22
|
+
from typing import AsyncIterator, Optional
|
|
23
|
+
import aiohttp
|
|
24
|
+
import aiofiles # type: ignore
|
|
25
|
+
import aiofiles.os # type: ignore
|
|
26
|
+
from loguru import logger # type: ignore
|
|
27
|
+
from PIL import Image
|
|
28
|
+
|
|
29
|
+
my_debug = os.environ.get("my_debug", None) is not None
|
|
30
|
+
if my_debug:
|
|
31
|
+
os.environ["HTTP_PROXY"] = "http://127.0.0.1:8888"
|
|
32
|
+
os.environ["HTTPS_PROXY"] = "http://127.0.0.1:8888"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_image_info(filename: str) -> tuple[Optional[str], tuple[int, int], str]:
|
|
36
|
+
with Image.open(filename) as im:
|
|
37
|
+
return (im.format, im.size, im.mode)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def compute_image_size(width: int, height: int, max_dimension=2000) -> tuple[int, int]:
|
|
41
|
+
width_changed, height_changed = (0, 0)
|
|
42
|
+
if (width <= max_dimension and height <= max_dimension) or (width == height):
|
|
43
|
+
return (width, height)
|
|
44
|
+
max_number: int = max(width, height)
|
|
45
|
+
divided_number: int = max_number / max_dimension
|
|
46
|
+
min_number: int = min(width, height)
|
|
47
|
+
min_side: int = floor(min_number / divided_number)
|
|
48
|
+
if width > height:
|
|
49
|
+
width_changed = max_dimension
|
|
50
|
+
height_changed = min_side
|
|
51
|
+
else:
|
|
52
|
+
width_changed = min_side
|
|
53
|
+
height_changed = max_dimension
|
|
54
|
+
return (width_changed, height_changed)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def process_image(
|
|
58
|
+
path_to_image: str,
|
|
59
|
+
path_to_processed_image: str,
|
|
60
|
+
max_dimension: int = 2000,
|
|
61
|
+
format: str = "JPEG",
|
|
62
|
+
optimize: bool = True,
|
|
63
|
+
jpeg_compress: int = 80,
|
|
64
|
+
) -> tuple[int, int]:
|
|
65
|
+
"""
|
|
66
|
+
Process the image for API requirements.
|
|
67
|
+
processed image will be saved in `path_to_processed_image`
|
|
68
|
+
as new file in the specified format (default JPEG) with compression 80% by default.
|
|
69
|
+
Returns the resolution of the image.
|
|
70
|
+
"""
|
|
71
|
+
logger.debug("start image process")
|
|
72
|
+
with open(path_to_image, "rb") as fp:
|
|
73
|
+
im = Image.open(fp)
|
|
74
|
+
width, height = im.size
|
|
75
|
+
if im.mode != "RGB":
|
|
76
|
+
logger.info(f"converting from {im.mode} to RGB mode")
|
|
77
|
+
im = im.convert("RGB")
|
|
78
|
+
if width > max_dimension or height > max_dimension:
|
|
79
|
+
logger.info("resizing image")
|
|
80
|
+
im.thumbnail(
|
|
81
|
+
compute_image_size(
|
|
82
|
+
width=width, height=height, max_dimension=max_dimension
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
im.save(
|
|
86
|
+
path_to_processed_image,
|
|
87
|
+
format.upper(),
|
|
88
|
+
optimize=optimize,
|
|
89
|
+
quality=jpeg_compress,
|
|
90
|
+
)
|
|
91
|
+
width, height = im.size
|
|
92
|
+
logger.info("image processed: %s, %dx%d" % (format, width, height))
|
|
93
|
+
im.close()
|
|
94
|
+
assert os.path.isfile(path_to_processed_image), "The processed file not found"
|
|
95
|
+
return (width, height)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
dl_folder = os.path.join(os.path.abspath("."), "downloads")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class BeMyAIError(Exception):
|
|
102
|
+
msg = None
|
|
103
|
+
response = None
|
|
104
|
+
|
|
105
|
+
def __init__(self, msg, response=None):
|
|
106
|
+
self.response = response
|
|
107
|
+
super().__init__(msg)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class EmailVerificationRequired(BeMyAIError):
|
|
111
|
+
msg = None
|
|
112
|
+
response = None
|
|
113
|
+
|
|
114
|
+
def __init__(self, msg="email verification required", response=None):
|
|
115
|
+
self.response = response
|
|
116
|
+
super().__init__(msg, response)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class PasswordChangeRequired(BeMyAIError):
|
|
120
|
+
msg = None
|
|
121
|
+
response = None
|
|
122
|
+
|
|
123
|
+
def __init__(self, msg="password change required", response=None):
|
|
124
|
+
self.response = response
|
|
125
|
+
super().__init__(msg, response)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class BeMyAI:
|
|
129
|
+
def __init__(
|
|
130
|
+
self, token: str = "", response_language="en-US", trust_env: bool = True
|
|
131
|
+
):
|
|
132
|
+
if not os.path.isdir(dl_folder):
|
|
133
|
+
os.mkdir(dl_folder)
|
|
134
|
+
logger.info("created dl folder")
|
|
135
|
+
else:
|
|
136
|
+
logger.info("dl folder already exists")
|
|
137
|
+
self.token = token
|
|
138
|
+
if response_language in LANGS or response_language in LOCALES:
|
|
139
|
+
self.response_language = response_language
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
"the language must be a two-letter code or a country separated by a hyphen"
|
|
143
|
+
)
|
|
144
|
+
self.trust_env: bool = trust_env
|
|
145
|
+
self.bemyeyes_app_secret = (
|
|
146
|
+
"55519e815ff7b09ab971de5564baa282eca53af1eb528385fb98a34f2010e8c7"
|
|
147
|
+
)
|
|
148
|
+
self.User_Agent = "okhttp/3.14.9"
|
|
149
|
+
self.gateway_url = "https://gateway.bemyeyes.com/api/v2/"
|
|
150
|
+
self.lp_delimeter = chr(30)
|
|
151
|
+
self.api_url = "https://api.bemyeyes.com/api/v2/"
|
|
152
|
+
self.sid = ""
|
|
153
|
+
self.extra = {
|
|
154
|
+
"device_type": "android",
|
|
155
|
+
"screen_reader_enabled": True,
|
|
156
|
+
"content_size": "1.15",
|
|
157
|
+
"accesibility_content_size_enabled": False,
|
|
158
|
+
}
|
|
159
|
+
self.app_config_user_cache: Optional[AppConfigUserModel] = None
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def get_error_messages(r):
|
|
163
|
+
error_messages = []
|
|
164
|
+
for m in r.items():
|
|
165
|
+
k, v = m
|
|
166
|
+
if isinstance(v, list):
|
|
167
|
+
v = ", ".join(v)
|
|
168
|
+
elif not isinstance(v, str):
|
|
169
|
+
v = str(v)
|
|
170
|
+
error_messages.append(": ".join([k, v]))
|
|
171
|
+
if len(error_messages) == 0:
|
|
172
|
+
return r
|
|
173
|
+
return "\n".join(error_messages)
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def headers(self):
|
|
177
|
+
h = {
|
|
178
|
+
"User-Agent": self.User_Agent,
|
|
179
|
+
"bemyeyes-app-secret": self.bemyeyes_app_secret,
|
|
180
|
+
"Accept-Language": self.response_language,
|
|
181
|
+
}
|
|
182
|
+
if self.token and len(self.token) > 3:
|
|
183
|
+
h.update(Authorization="Token " + self.token)
|
|
184
|
+
return h
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def terms_accepted_at(self) -> str:
|
|
188
|
+
"For signup"
|
|
189
|
+
shanghai_offset = timedelta(hours=8)
|
|
190
|
+
utc_now = datetime.now(timezone.utc)
|
|
191
|
+
shanghai_now = utc_now.astimezone(timezone(shanghai_offset))
|
|
192
|
+
logger.debug("date for signup: " + str(shanghai_now))
|
|
193
|
+
return shanghai_now.isoformat()
|
|
194
|
+
|
|
195
|
+
async def request(
|
|
196
|
+
self, method, url, params=None, data=None, json=None, headers=None
|
|
197
|
+
):
|
|
198
|
+
"""
|
|
199
|
+
Make a request to the API.
|
|
200
|
+
In the response from the gateway server you Get the text,
|
|
201
|
+
since the server returns data in different formats and with different headers,
|
|
202
|
+
and from the api server get a dictionary,
|
|
203
|
+
since all interaction is strictly in JSON format with the correct headers
|
|
204
|
+
Thanks to Django REST framework on their side.
|
|
205
|
+
"""
|
|
206
|
+
j = None
|
|
207
|
+
if headers is None:
|
|
208
|
+
headers = self.headers
|
|
209
|
+
logger.debug(f"making {method} request to {url}...")
|
|
210
|
+
async with aiohttp.ClientSession(
|
|
211
|
+
trust_env=self.trust_env, raise_for_status=False, headers=headers
|
|
212
|
+
) as session:
|
|
213
|
+
async with session.request(
|
|
214
|
+
method=method, url=url, params=params, data=data, json=json
|
|
215
|
+
) as resp:
|
|
216
|
+
if "json" not in resp.headers.get("Content-Type").lower():
|
|
217
|
+
if resp.ok:
|
|
218
|
+
return await resp.text()
|
|
219
|
+
else:
|
|
220
|
+
raise BeMyAIError(message=resp.text, response=resp)
|
|
221
|
+
else:
|
|
222
|
+
if resp.ok:
|
|
223
|
+
return await resp.json()
|
|
224
|
+
else:
|
|
225
|
+
j = await resp.json()
|
|
226
|
+
if j.get("code", 0) != 1:
|
|
227
|
+
raise BeMyAIError(
|
|
228
|
+
msg=self.get_error_messages(j), response=resp
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
logger.debug("There will be a new session next time")
|
|
232
|
+
await self.get_chat_config()
|
|
233
|
+
await self.authinticate(self.sid)
|
|
234
|
+
await self.enable_chat(self.sid)
|
|
235
|
+
return await self.request(
|
|
236
|
+
method=method,
|
|
237
|
+
url=url,
|
|
238
|
+
params=params,
|
|
239
|
+
data=data,
|
|
240
|
+
json=json,
|
|
241
|
+
headers=headers,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
async def refresh_token(self) -> LoginResponseModel:
|
|
245
|
+
"Check the user (for example, to find out if the email address is verified)"
|
|
246
|
+
resp = await self.request(
|
|
247
|
+
"POST",
|
|
248
|
+
self.api_url + "auth/refresh-token",
|
|
249
|
+
json={"timezone": "Asia/Shanghai", "extra": self.extra},
|
|
250
|
+
)
|
|
251
|
+
result = LoginResponseModel.model_validate(resp)
|
|
252
|
+
self.token = result.token
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
async def resend_verify_email(self) -> None:
|
|
256
|
+
"Send a confirmation email again (only after signup)"
|
|
257
|
+
result = await self.refresh_token()
|
|
258
|
+
if not result.email_verification_required:
|
|
259
|
+
raise BeMyAIError("The email has already been verified")
|
|
260
|
+
await self.request("POST", self.api_url + "auth/resend-verify-email")
|
|
261
|
+
|
|
262
|
+
async def send_reset_password(self, email: str) -> None:
|
|
263
|
+
"Recover a forgotten password. A link to the password change form will be sent to your email"
|
|
264
|
+
self.token = ""
|
|
265
|
+
await self.request(
|
|
266
|
+
"POST", self.api_url + "auth/send-reset-password", json={"email": email}
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
async def signup(
|
|
270
|
+
self, first_name: str, last_name: str, email: str, password: str
|
|
271
|
+
) -> LoginResponseModel:
|
|
272
|
+
"Signup by mail and password (auth/signup-email) and get user object"
|
|
273
|
+
self.token = ""
|
|
274
|
+
resp = await self.request(
|
|
275
|
+
"POST",
|
|
276
|
+
self.api_url + "auth/signup-email",
|
|
277
|
+
json={
|
|
278
|
+
"first_name": first_name,
|
|
279
|
+
"last_name": last_name,
|
|
280
|
+
"email": email,
|
|
281
|
+
"password": password,
|
|
282
|
+
"user_type": "bvi",
|
|
283
|
+
"timezone": "Asia/Shanghai",
|
|
284
|
+
"terms_accepted_at": self.terms_accepted_at,
|
|
285
|
+
"extra": self.extra,
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
result = LoginResponseModel.model_validate(resp)
|
|
289
|
+
self.token = result.token
|
|
290
|
+
return result
|
|
291
|
+
|
|
292
|
+
async def login(self, email, password):
|
|
293
|
+
"Login by mail and password (auth/login-email) and get token"
|
|
294
|
+
self.token = ""
|
|
295
|
+
resp = await self.request(
|
|
296
|
+
"POST",
|
|
297
|
+
self.api_url + "auth/login-email",
|
|
298
|
+
json={"email": email, "password": password, "extra": self.extra},
|
|
299
|
+
)
|
|
300
|
+
result = LoginResponseModel.model_validate(resp)
|
|
301
|
+
self.token = result.token
|
|
302
|
+
if result.email_verification_required:
|
|
303
|
+
raise EmailVerificationRequired(response=result)
|
|
304
|
+
if result.password_change_required:
|
|
305
|
+
raise PasswordChangeRequired(response=result)
|
|
306
|
+
return result
|
|
307
|
+
|
|
308
|
+
async def app_config_user(self) -> AppConfigUserModel:
|
|
309
|
+
"""
|
|
310
|
+
Get the application configuration for the user.
|
|
311
|
+
The results will be cached in the class instance.
|
|
312
|
+
"""
|
|
313
|
+
if self.app_config_user_cache:
|
|
314
|
+
logger.info("get app user config from cache")
|
|
315
|
+
return self.app_config_user_cache
|
|
316
|
+
resp = await self.request("GET", self.api_url + "app-config/user")
|
|
317
|
+
self.app_config_user_cache = AppConfigUserModel.model_validate(resp) # type: ignore
|
|
318
|
+
logger.info("get app user config from internet")
|
|
319
|
+
return self.app_config_user_cache # type: ignore
|
|
320
|
+
|
|
321
|
+
async def send_text_message(
|
|
322
|
+
self, chat_id: int, text: str
|
|
323
|
+
) -> tuple[str, int, ChatMessagesModel]:
|
|
324
|
+
"Send a text message to the chat"
|
|
325
|
+
if not isinstance(chat_id, int):
|
|
326
|
+
raise TabError("chat_id must be int")
|
|
327
|
+
if not isinstance(text, str):
|
|
328
|
+
raise TabError("text must be str")
|
|
329
|
+
await self.get_chat_config()
|
|
330
|
+
await self.authinticate(self.sid)
|
|
331
|
+
await self.enable_chat(self.sid)
|
|
332
|
+
resp = await self.request(
|
|
333
|
+
"POST",
|
|
334
|
+
f"{self.api_url}chats/{chat_id}/messages",
|
|
335
|
+
json={"role": "user", "type": "text", "data": text},
|
|
336
|
+
)
|
|
337
|
+
result = ChatMessagesModel.model_validate(resp)
|
|
338
|
+
return (self.sid, result.session, result)
|
|
339
|
+
|
|
340
|
+
async def chats(self, context="chat") -> ChatModel:
|
|
341
|
+
"""
|
|
342
|
+
Create new chat
|
|
343
|
+
"""
|
|
344
|
+
logger.info("create new chat")
|
|
345
|
+
resp = await self.request(
|
|
346
|
+
"POST", self.api_url + "chats", json={"context": context}
|
|
347
|
+
)
|
|
348
|
+
return ChatModel.model_validate(resp)
|
|
349
|
+
|
|
350
|
+
async def chat_messages(self, chat_id: int, image_id: int) -> ChatMessagesModel:
|
|
351
|
+
resp = await self.request(
|
|
352
|
+
"POST",
|
|
353
|
+
f"{self.api_url}chats/{chat_id}/messages",
|
|
354
|
+
json={"role": "user", "type": "text", "chat_image_id": image_id},
|
|
355
|
+
)
|
|
356
|
+
return ChatMessagesModel.model_validate(resp)
|
|
357
|
+
|
|
358
|
+
async def chat_request_image_upload(
|
|
359
|
+
self, chat_id: int, width: int, height: int, format: str = "jpeg"
|
|
360
|
+
) -> ChatUploadImageConfigModel:
|
|
361
|
+
"""
|
|
362
|
+
Request parameters for upload image to the chat
|
|
363
|
+
"""
|
|
364
|
+
logger.info("requested upload image config")
|
|
365
|
+
cnf = await self.app_config_user()
|
|
366
|
+
if width < 16 or height < 16:
|
|
367
|
+
raise ValueError("The image is too small")
|
|
368
|
+
if (
|
|
369
|
+
width > cnf.chat_image_max_dimension
|
|
370
|
+
or height > cnf.chat_image_max_dimension
|
|
371
|
+
):
|
|
372
|
+
raise ValueError(
|
|
373
|
+
f"width or height must be less than or equal to {cnf.chat_image_max_dimension}"
|
|
374
|
+
)
|
|
375
|
+
resp = await self.request(
|
|
376
|
+
"POST",
|
|
377
|
+
f"{self.api_url}chats/{chat_id}/request-image-upload",
|
|
378
|
+
json={"format": format, "width": width, "height": height},
|
|
379
|
+
)
|
|
380
|
+
return ChatUploadImageConfigModel.model_validate(resp)
|
|
381
|
+
|
|
382
|
+
async def take_photo(self, filename: str) -> tuple[str, int]:
|
|
383
|
+
path_to_image = None
|
|
384
|
+
if not isinstance(filename, str):
|
|
385
|
+
path_to_image = os.path.join(dl_folder, f"image_{str(uuid4())}.tmp")
|
|
386
|
+
with open(path_to_image, "wb") as newfp:
|
|
387
|
+
shutil.copyfileobj(filename, newfp)
|
|
388
|
+
filename = path_to_image # type: ignore
|
|
389
|
+
loop = asyncio.get_running_loop()
|
|
390
|
+
cnf = await self.app_config_user()
|
|
391
|
+
format, size, mode = await loop.run_in_executor(
|
|
392
|
+
None, partial(get_image_info, filename=filename)
|
|
393
|
+
)
|
|
394
|
+
logger.info(
|
|
395
|
+
"recognizing new photo: %s, %s, %s"
|
|
396
|
+
% (format, str(size[0]) + "x" + str(size[1]), mode)
|
|
397
|
+
)
|
|
398
|
+
chat_config = await self.get_chat_config()
|
|
399
|
+
await self.authinticate(chat_config.sid)
|
|
400
|
+
await self.enable_chat(chat_config.sid)
|
|
401
|
+
chat = await self.chats()
|
|
402
|
+
path_to_processed_image = os.path.join(
|
|
403
|
+
dl_folder, f"processed_image_{str(uuid4())}.{cnf.chat_image_type}"
|
|
404
|
+
)
|
|
405
|
+
width, height = await loop.run_in_executor(
|
|
406
|
+
None,
|
|
407
|
+
partial(
|
|
408
|
+
process_image,
|
|
409
|
+
path_to_image=filename,
|
|
410
|
+
path_to_processed_image=path_to_processed_image,
|
|
411
|
+
max_dimension=cnf.chat_image_max_dimension,
|
|
412
|
+
format=cnf.chat_image_type,
|
|
413
|
+
optimize=(cnf.chat_image_type.upper() == "JPEG"),
|
|
414
|
+
jpeg_compress=cnf.chat_image_jpeg_compression,
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
upload_config = await self.chat_request_image_upload(
|
|
418
|
+
chat_id=chat.id,
|
|
419
|
+
width=width,
|
|
420
|
+
height=height,
|
|
421
|
+
format=cnf.chat_image_type,
|
|
422
|
+
)
|
|
423
|
+
logger.info("Starting upload image to Amazon")
|
|
424
|
+
fd = aiohttp.FormData(quote_fields=False)
|
|
425
|
+
fields = dict(
|
|
426
|
+
{
|
|
427
|
+
"Content-Type": upload_config.fields.Content_Type,
|
|
428
|
+
"key": upload_config.fields.key,
|
|
429
|
+
"x-amz-algorithm": upload_config.fields.x_amz_algorithm,
|
|
430
|
+
"x-amz-credential": upload_config.fields.x_amz_credential,
|
|
431
|
+
"x-amz-date": upload_config.fields.x_amz_date,
|
|
432
|
+
"policy": upload_config.fields.policy,
|
|
433
|
+
"x-amz-signature": upload_config.fields.x_amz_signature,
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
for field in fields.items():
|
|
437
|
+
fd.add_field(*field, content_type=None, content_transfer_encoding=None)
|
|
438
|
+
with open(path_to_processed_image, "rb") as fp:
|
|
439
|
+
fd.add_field(
|
|
440
|
+
"file",
|
|
441
|
+
fp,
|
|
442
|
+
content_type=upload_config.fields.Content_Type,
|
|
443
|
+
filename="file",
|
|
444
|
+
)
|
|
445
|
+
async with aiohttp.ClientSession(
|
|
446
|
+
trust_env=self.trust_env,
|
|
447
|
+
raise_for_status=True,
|
|
448
|
+
headers={"User-Agent": self.User_Agent},
|
|
449
|
+
) as session:
|
|
450
|
+
async with session.post(str(upload_config.url), data=fd) as resp:
|
|
451
|
+
if resp.status >= 100 <= 206:
|
|
452
|
+
logger.info("Uploaded successfully")
|
|
453
|
+
else:
|
|
454
|
+
logger.error("Upload faild")
|
|
455
|
+
_ = await resp.read()
|
|
456
|
+
logger.info("removeing processed image")
|
|
457
|
+
await aiofiles.os.remove(path_to_processed_image)
|
|
458
|
+
if path_to_image:
|
|
459
|
+
logger.info("removeing tmp image")
|
|
460
|
+
await aiofiles.os.remove(path_to_image)
|
|
461
|
+
upload_result = await self.chat_messages(
|
|
462
|
+
chat_id=chat.id, image_id=upload_config.chat_image_id
|
|
463
|
+
)
|
|
464
|
+
if not upload_result.images[0].upload_finished:
|
|
465
|
+
logger.error("The image has not been uploaded")
|
|
466
|
+
raise BeMyAIError("The image could not be uploaded to Amazon")
|
|
467
|
+
else:
|
|
468
|
+
logger.info("upload image finished")
|
|
469
|
+
return (chat_config.sid, chat.id)
|
|
470
|
+
|
|
471
|
+
async def send_raw_events(self, sid: str, data: str) -> str:
|
|
472
|
+
params = {"EIO": "4", "transport": "polling", "sid": sid}
|
|
473
|
+
text = await self.request(
|
|
474
|
+
"POST", self.gateway_url + "socket/", params=params, data=data
|
|
475
|
+
)
|
|
476
|
+
return text
|
|
477
|
+
|
|
478
|
+
async def authinticate(self, sid: str = "") -> bool:
|
|
479
|
+
if not sid:
|
|
480
|
+
sid = self.sid
|
|
481
|
+
text = await self.send_raw_events(sid, "40")
|
|
482
|
+
if text != "ok":
|
|
483
|
+
raise BeMyAIError(f"An unexpected response was received. {text}")
|
|
484
|
+
text = await self.receive_raw_events(sid)
|
|
485
|
+
if "AUTHENTICATED" not in text:
|
|
486
|
+
raise BeMyAIError(f"An unexpected response was received. {text}")
|
|
487
|
+
return True
|
|
488
|
+
|
|
489
|
+
async def get_chat_config(self) -> ChatConfigModel:
|
|
490
|
+
"Get sid and other settings"
|
|
491
|
+
logger.debug("Getting chat config...")
|
|
492
|
+
text = await self.receive_raw_events(get_new_sid=True)
|
|
493
|
+
result = ChatConfigModel.model_validate_json(text[1::])
|
|
494
|
+
if self.sid != result.sid:
|
|
495
|
+
logger.debug("received new session id")
|
|
496
|
+
self.sid = result.sid
|
|
497
|
+
return result
|
|
498
|
+
|
|
499
|
+
async def enable_chat(self, sid: str = "") -> str:
|
|
500
|
+
logger.debug("enabling the chat")
|
|
501
|
+
if not sid:
|
|
502
|
+
sid = self.sid
|
|
503
|
+
return await self.send_raw_events(sid, '42["ENABLE_CHAT","{}"]')
|
|
504
|
+
|
|
505
|
+
async def receive_raw_events(self, sid: str = "", get_new_sid=False) -> str:
|
|
506
|
+
params = {"EIO": "4", "transport": "polling"}
|
|
507
|
+
if not sid:
|
|
508
|
+
sid = self.sid
|
|
509
|
+
if sid and not get_new_sid:
|
|
510
|
+
params["sid"] = sid
|
|
511
|
+
text = await self.request("GET", self.gateway_url + "socket/", params=params)
|
|
512
|
+
return text
|
|
513
|
+
|
|
514
|
+
async def receive_messages(self, sid: str = "") -> AsyncIterator[ChatMessagesModel]:
|
|
515
|
+
logger.debug("Starting receive GPT messages")
|
|
516
|
+
if not sid:
|
|
517
|
+
sid = self.sid
|
|
518
|
+
for i in range(3):
|
|
519
|
+
text = await self.receive_raw_events(sid)
|
|
520
|
+
if "42" not in text:
|
|
521
|
+
logger.debug("unexpected response from polling: " + str(text))
|
|
522
|
+
continue
|
|
523
|
+
for response_part in text.split(self.lp_delimeter):
|
|
524
|
+
if '"NEW_CHAT_MESSAGE"' in response_part:
|
|
525
|
+
message = ChatMessagesModel.model_validate(
|
|
526
|
+
json.loads(response_part[2:])[1]
|
|
527
|
+
)
|
|
528
|
+
logger.info("Got new message")
|
|
529
|
+
yield message
|
|
530
|
+
if not message.user:
|
|
531
|
+
break
|
|
532
|
+
else:
|
|
533
|
+
logger.debug(response_part[2:-1])
|
|
534
|
+
logger.debug("messages receiveing completed")
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import os.path
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from bemyai import BeMyAI # type: ignore
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def _messages_receiver(p, bm, sid, chat_id):
|
|
11
|
+
while True:
|
|
12
|
+
async for bm_response in bm.receive_messages(sid):
|
|
13
|
+
message = bm_response
|
|
14
|
+
if message.user:
|
|
15
|
+
continue
|
|
16
|
+
print(message.data)
|
|
17
|
+
break
|
|
18
|
+
if p.ni:
|
|
19
|
+
break
|
|
20
|
+
print("Type Q and press enter for exit")
|
|
21
|
+
text = input("Message:")
|
|
22
|
+
if text.strip().lower() in ["q", "quit", "exit"]:
|
|
23
|
+
break
|
|
24
|
+
sid, chat_id, _message = await bm.send_text_message(chat_id, text)
|
|
25
|
+
await asyncio.sleep(0.1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def a_main(args: list) -> None:
|
|
29
|
+
parser = argparse.ArgumentParser(prog="bm")
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--lang",
|
|
32
|
+
"-l",
|
|
33
|
+
dest="lang",
|
|
34
|
+
default="en",
|
|
35
|
+
metavar="en",
|
|
36
|
+
help="Response language",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--tokenfile",
|
|
40
|
+
"-tf",
|
|
41
|
+
dest="tokenfile",
|
|
42
|
+
default=os.path.expanduser(os.path.join("~", "bm_token.txt")),
|
|
43
|
+
metavar="bm_token.txt",
|
|
44
|
+
help="the path to the file for storing the token from the account",
|
|
45
|
+
type=argparse.FileType("a+b"),
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--sessionfile",
|
|
49
|
+
"-sf",
|
|
50
|
+
dest="sessionfile",
|
|
51
|
+
default=os.path.expanduser(os.path.join("~", "bm_session.txt")),
|
|
52
|
+
metavar="bm_session.txt",
|
|
53
|
+
help="the path to the file for storing the session id",
|
|
54
|
+
type=argparse.FileType("a+b"),
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--not-interactive",
|
|
58
|
+
"-ni",
|
|
59
|
+
dest="ni",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="Do not ask for user input",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"-v", "--verbose", action="store_true", help="Whether to enable logging?"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
subparsers = parser.add_subparsers(dest="sub", help="Action")
|
|
68
|
+
parser_login = subparsers.add_parser(
|
|
69
|
+
"login", help="Log in to your account and get a token"
|
|
70
|
+
)
|
|
71
|
+
parser_login.add_argument("email", metavar="E-mail address")
|
|
72
|
+
parser_login.add_argument("password", metavar="Password")
|
|
73
|
+
|
|
74
|
+
parser_signup = subparsers.add_parser("signup", help="Create a new account")
|
|
75
|
+
parser_signup.add_argument("first_name", metavar="First name")
|
|
76
|
+
parser_signup.add_argument("last_name", metavar="Last name")
|
|
77
|
+
|
|
78
|
+
parser_signup.add_argument("email", metavar="E-mail address")
|
|
79
|
+
parser_signup.add_argument("password", metavar="Password")
|
|
80
|
+
|
|
81
|
+
parser_reset_password = subparsers.add_parser(
|
|
82
|
+
"reset-password",
|
|
83
|
+
help="Forgot your password? Send a link to the password change form by email",
|
|
84
|
+
)
|
|
85
|
+
parser_reset_password.add_argument("email", metavar="E-mail address")
|
|
86
|
+
|
|
87
|
+
subparsers.add_parser(
|
|
88
|
+
"resend-verify-email", help="Send the confirmation email again"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
parser_recognize = subparsers.add_parser(
|
|
92
|
+
"recognize", help="Get a description for an image"
|
|
93
|
+
)
|
|
94
|
+
parser_recognize.add_argument("photo", type=argparse.FileType("rb"))
|
|
95
|
+
|
|
96
|
+
parser_ask = subparsers.add_parser(
|
|
97
|
+
"ask", help="Ask a question about recognized photo"
|
|
98
|
+
)
|
|
99
|
+
parser_ask.add_argument("text", help="message")
|
|
100
|
+
|
|
101
|
+
p = parser.parse_args()
|
|
102
|
+
if p.verbose:
|
|
103
|
+
logger.remove()
|
|
104
|
+
logger.add(sys.stderr, format="{time} {level} {message}", level="DEBUG")
|
|
105
|
+
else:
|
|
106
|
+
logger.remove()
|
|
107
|
+
if p.sub == "login":
|
|
108
|
+
bm = BeMyAI("", response_language=p.lang)
|
|
109
|
+
result = await bm.login(email=p.email, password=p.password)
|
|
110
|
+
p.tokenfile.seek(0)
|
|
111
|
+
p.tokenfile.truncate()
|
|
112
|
+
p.tokenfile.write(result.token.encode("UTF-8"))
|
|
113
|
+
p.tokenfile.flush()
|
|
114
|
+
|
|
115
|
+
if p.sub == "signup":
|
|
116
|
+
bm = BeMyAI("", response_language=p.lang)
|
|
117
|
+
result = await bm.signup(
|
|
118
|
+
first_name=p.first_name,
|
|
119
|
+
last_name=p.last_name,
|
|
120
|
+
email=p.email,
|
|
121
|
+
password=p.password,
|
|
122
|
+
)
|
|
123
|
+
p.tokenfile.seek(0)
|
|
124
|
+
p.tokenfile.truncate()
|
|
125
|
+
p.tokenfile.write(result.token.encode("UTF-8"))
|
|
126
|
+
p.tokenfile.flush()
|
|
127
|
+
|
|
128
|
+
sys.stderr.write("Don't forget to confirm your email address")
|
|
129
|
+
|
|
130
|
+
if p.sub == "reset-password":
|
|
131
|
+
bm = BeMyAI("", response_language=p.lang)
|
|
132
|
+
await bm.send_reset_password(email=p.email)
|
|
133
|
+
|
|
134
|
+
if p.sub == "resend-verify-email":
|
|
135
|
+
p.tokenfile.seek(0)
|
|
136
|
+
token = p.tokenfile.read().decode("UTF-8")
|
|
137
|
+
bm = BeMyAI(token, response_language=p.lang)
|
|
138
|
+
await bm.resend_verify_email()
|
|
139
|
+
|
|
140
|
+
if p.sub == "recognize":
|
|
141
|
+
p.tokenfile.seek(0)
|
|
142
|
+
token = p.tokenfile.read().decode("UTF-8")
|
|
143
|
+
bm = BeMyAI(token, response_language=p.lang)
|
|
144
|
+
sid, chat_id = await bm.take_photo(p.photo)
|
|
145
|
+
p.sessionfile.seek(0)
|
|
146
|
+
p.sessionfile.truncate()
|
|
147
|
+
p.sessionfile.write(str(chat_id).encode("UTF-8"))
|
|
148
|
+
p.sessionfile.flush()
|
|
149
|
+
await _messages_receiver(p, bm, sid, chat_id)
|
|
150
|
+
if p.sub == "ask":
|
|
151
|
+
p.sessionfile.seek(0)
|
|
152
|
+
try:
|
|
153
|
+
chat_id = int(p.sessionfile.read())
|
|
154
|
+
except ValueError:
|
|
155
|
+
sys.stderr.write("session id not found")
|
|
156
|
+
sys.exit(2)
|
|
157
|
+
p.tokenfile.seek(0)
|
|
158
|
+
token = p.tokenfile.read().decode("UTF-8")
|
|
159
|
+
bm = BeMyAI(token, response_language=p.lang)
|
|
160
|
+
sid, chat_id, _message = await bm.send_text_message(chat_id, p.text)
|
|
161
|
+
await _messages_receiver(p, bm, sid, chat_id)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main(args: list = sys.argv) -> None:
|
|
165
|
+
asyncio.run(a_main(args))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
main(sys.argv)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
__all__ = ["LOCALES", "LANGS"]
|
|
2
|
+
|
|
3
|
+
LOCALES: list = [
|
|
4
|
+
"lv-LV",
|
|
5
|
+
"es-BO",
|
|
6
|
+
"ar-SA",
|
|
7
|
+
"mt-MT",
|
|
8
|
+
"hi-IN",
|
|
9
|
+
"bn-IN",
|
|
10
|
+
"bs-BA",
|
|
11
|
+
"ca-ES",
|
|
12
|
+
"ka-GE",
|
|
13
|
+
"kn-IN",
|
|
14
|
+
"si-LK",
|
|
15
|
+
"en-TZ",
|
|
16
|
+
"ms-MY",
|
|
17
|
+
"en-ZA",
|
|
18
|
+
"it-IT",
|
|
19
|
+
"gu-IN",
|
|
20
|
+
"pl-PL",
|
|
21
|
+
"ga-IE",
|
|
22
|
+
"bg-BG",
|
|
23
|
+
"lo-LA",
|
|
24
|
+
"ar-SY",
|
|
25
|
+
"af-ZA",
|
|
26
|
+
"ar-AE",
|
|
27
|
+
"da-DK",
|
|
28
|
+
"mr-IN",
|
|
29
|
+
"es-VE",
|
|
30
|
+
"es-EC",
|
|
31
|
+
"en-KE",
|
|
32
|
+
"pt-BR",
|
|
33
|
+
"ru-RU",
|
|
34
|
+
"mn-MN",
|
|
35
|
+
"en-GB",
|
|
36
|
+
"sl-SI",
|
|
37
|
+
"en-NG",
|
|
38
|
+
"zh-CN",
|
|
39
|
+
"ar-LY",
|
|
40
|
+
"bn-BD",
|
|
41
|
+
"fr-CH",
|
|
42
|
+
"zu-ZA",
|
|
43
|
+
"ne-NP",
|
|
44
|
+
"es-MX",
|
|
45
|
+
"cy-GB",
|
|
46
|
+
"tr-TR",
|
|
47
|
+
"km-KH",
|
|
48
|
+
"es-DO",
|
|
49
|
+
"es-CL",
|
|
50
|
+
"uz-UZ",
|
|
51
|
+
"ta-MY",
|
|
52
|
+
"fi-FI",
|
|
53
|
+
"el-GR",
|
|
54
|
+
"ar-JO",
|
|
55
|
+
"zh-HK",
|
|
56
|
+
"ar-TN",
|
|
57
|
+
"es-NI",
|
|
58
|
+
"es-PY",
|
|
59
|
+
"ar-IQ",
|
|
60
|
+
"ml-IN",
|
|
61
|
+
"ar-YE",
|
|
62
|
+
"en-AU",
|
|
63
|
+
"fr-BE",
|
|
64
|
+
"es-CR",
|
|
65
|
+
"ur-IN",
|
|
66
|
+
"fr-CA",
|
|
67
|
+
"en-HK",
|
|
68
|
+
"ur-PK",
|
|
69
|
+
"ar-DZ",
|
|
70
|
+
"lt-LT",
|
|
71
|
+
"ar-KW",
|
|
72
|
+
"es-HN",
|
|
73
|
+
"ar-BH",
|
|
74
|
+
"th-TH",
|
|
75
|
+
"ta-IN",
|
|
76
|
+
"id-ID",
|
|
77
|
+
"es-AR",
|
|
78
|
+
"fa-IR",
|
|
79
|
+
"nl-BE",
|
|
80
|
+
"es-UY",
|
|
81
|
+
"en-IE",
|
|
82
|
+
"ps-AF",
|
|
83
|
+
"cs-CZ",
|
|
84
|
+
"su-ID",
|
|
85
|
+
"ko-KR",
|
|
86
|
+
"de-DE",
|
|
87
|
+
"es-PR",
|
|
88
|
+
"es-ES",
|
|
89
|
+
"es-PA",
|
|
90
|
+
"kk-KZ",
|
|
91
|
+
"ar-LB",
|
|
92
|
+
"vi-VN",
|
|
93
|
+
"es-CO",
|
|
94
|
+
"my-MM",
|
|
95
|
+
"sk-SK",
|
|
96
|
+
"et-EE",
|
|
97
|
+
"sw-TZ",
|
|
98
|
+
"sv-SE",
|
|
99
|
+
"es-GQ",
|
|
100
|
+
"es-US",
|
|
101
|
+
"te-IN",
|
|
102
|
+
"fr-FR",
|
|
103
|
+
"es-GT",
|
|
104
|
+
"hr-HR",
|
|
105
|
+
"es-CU",
|
|
106
|
+
"uk-UA",
|
|
107
|
+
"ta-SG",
|
|
108
|
+
"jv-ID",
|
|
109
|
+
"sr-RS",
|
|
110
|
+
"mk-MK",
|
|
111
|
+
"ar-MA",
|
|
112
|
+
"sq-AL",
|
|
113
|
+
"zh-TW",
|
|
114
|
+
"ja-JP",
|
|
115
|
+
"nl-NL",
|
|
116
|
+
"es-PE",
|
|
117
|
+
"am-ET",
|
|
118
|
+
"en-NZ",
|
|
119
|
+
"gl-ES",
|
|
120
|
+
"sw-KE",
|
|
121
|
+
"ro-RO",
|
|
122
|
+
"en-CA",
|
|
123
|
+
"so-SO",
|
|
124
|
+
"es-SV",
|
|
125
|
+
"nb-NO",
|
|
126
|
+
"en-IN",
|
|
127
|
+
"az-AZ",
|
|
128
|
+
"de-AT",
|
|
129
|
+
"he-IL",
|
|
130
|
+
"hu-HU",
|
|
131
|
+
"ar-EG",
|
|
132
|
+
"de-CH",
|
|
133
|
+
"ar-OM",
|
|
134
|
+
"is-IS",
|
|
135
|
+
"fil-PH",
|
|
136
|
+
"en-SG",
|
|
137
|
+
"en-PH",
|
|
138
|
+
"en-US",
|
|
139
|
+
"pt-PT",
|
|
140
|
+
"ta-LK",
|
|
141
|
+
"ar-QA",
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
LANGS: list = [
|
|
145
|
+
"hu",
|
|
146
|
+
"su",
|
|
147
|
+
"te",
|
|
148
|
+
"kn",
|
|
149
|
+
"pl",
|
|
150
|
+
"mt",
|
|
151
|
+
"ka",
|
|
152
|
+
"fi",
|
|
153
|
+
"de",
|
|
154
|
+
"sv",
|
|
155
|
+
"mn",
|
|
156
|
+
"zh",
|
|
157
|
+
"he",
|
|
158
|
+
"bg",
|
|
159
|
+
"ga",
|
|
160
|
+
"fr",
|
|
161
|
+
"es",
|
|
162
|
+
"th",
|
|
163
|
+
"kk",
|
|
164
|
+
"vi",
|
|
165
|
+
"mr",
|
|
166
|
+
"az",
|
|
167
|
+
"ml",
|
|
168
|
+
"km",
|
|
169
|
+
"ps",
|
|
170
|
+
"hr",
|
|
171
|
+
"ro",
|
|
172
|
+
"fil",
|
|
173
|
+
"bs",
|
|
174
|
+
"lv",
|
|
175
|
+
"lo",
|
|
176
|
+
"af",
|
|
177
|
+
"da",
|
|
178
|
+
"cs",
|
|
179
|
+
"ms",
|
|
180
|
+
"nb",
|
|
181
|
+
"ta",
|
|
182
|
+
"am",
|
|
183
|
+
"ko",
|
|
184
|
+
"so",
|
|
185
|
+
"hi",
|
|
186
|
+
"gl",
|
|
187
|
+
"pt",
|
|
188
|
+
"uz",
|
|
189
|
+
"uk",
|
|
190
|
+
"zu",
|
|
191
|
+
"si",
|
|
192
|
+
"lt",
|
|
193
|
+
"sk",
|
|
194
|
+
"bn",
|
|
195
|
+
"en",
|
|
196
|
+
"it",
|
|
197
|
+
"sw",
|
|
198
|
+
"el",
|
|
199
|
+
"ca",
|
|
200
|
+
"et",
|
|
201
|
+
"sr",
|
|
202
|
+
"mk",
|
|
203
|
+
"sq",
|
|
204
|
+
"ja",
|
|
205
|
+
"gu",
|
|
206
|
+
"nl",
|
|
207
|
+
"is",
|
|
208
|
+
"id",
|
|
209
|
+
"tr",
|
|
210
|
+
"ar",
|
|
211
|
+
"jv",
|
|
212
|
+
"ru",
|
|
213
|
+
"fa",
|
|
214
|
+
"my",
|
|
215
|
+
"sl",
|
|
216
|
+
"ur",
|
|
217
|
+
"cy",
|
|
218
|
+
"ne",
|
|
219
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Dict, List, Optional, Any
|
|
3
|
+
|
|
4
|
+
from pydantic import (
|
|
5
|
+
BaseModel,
|
|
6
|
+
Field,
|
|
7
|
+
StrictBool,
|
|
8
|
+
PositiveInt,
|
|
9
|
+
HttpUrl,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"UserModel",
|
|
14
|
+
"LoginResponseModel",
|
|
15
|
+
"AppConfigUserModel",
|
|
16
|
+
"WaitingListModel",
|
|
17
|
+
"ChatConfigModel",
|
|
18
|
+
"ChatModel",
|
|
19
|
+
"ChatUploadImageConfigModel",
|
|
20
|
+
"ChatMessagesModel",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UserModel(BaseModel):
|
|
25
|
+
id: int
|
|
26
|
+
uid: str
|
|
27
|
+
created_at: datetime
|
|
28
|
+
first_name: str
|
|
29
|
+
last_name: str
|
|
30
|
+
email: str
|
|
31
|
+
user_type: str
|
|
32
|
+
timezone: str
|
|
33
|
+
primary_language: str
|
|
34
|
+
secondary_languages: List
|
|
35
|
+
country: str
|
|
36
|
+
auth_type: str
|
|
37
|
+
has_usable_password: StrictBool
|
|
38
|
+
has_accepted_latest_terms: StrictBool
|
|
39
|
+
has_accepted_marketing: Optional[StrictBool]
|
|
40
|
+
is_pending_deletion: StrictBool
|
|
41
|
+
has_hidden_email: StrictBool
|
|
42
|
+
extra: Dict[str, Any]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LoginResponseModel(BaseModel):
|
|
46
|
+
user: UserModel
|
|
47
|
+
token: str
|
|
48
|
+
email_verification_required: StrictBool
|
|
49
|
+
password_change_required: StrictBool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ChatUploadImageConfigModelFields(BaseModel):
|
|
53
|
+
Content_Type: str = Field(alias="Content-Type")
|
|
54
|
+
key: str
|
|
55
|
+
x_amz_algorithm: str = Field(alias="x-amz-algorithm")
|
|
56
|
+
x_amz_credential: str = Field(alias="x-amz-credential")
|
|
57
|
+
x_amz_date: str = Field(alias="x-amz-date")
|
|
58
|
+
policy: str
|
|
59
|
+
x_amz_signature: str = Field(alias="x-amz-signature")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ChatUploadImageConfigModel(BaseModel):
|
|
63
|
+
url: HttpUrl
|
|
64
|
+
fields: ChatUploadImageConfigModelFields
|
|
65
|
+
chat_image_id: int
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ChatModel(BaseModel):
|
|
69
|
+
id: int
|
|
70
|
+
created_at: datetime
|
|
71
|
+
context: str
|
|
72
|
+
organization: Optional[str]
|
|
73
|
+
user: int
|
|
74
|
+
language: str
|
|
75
|
+
rating: Optional[int]
|
|
76
|
+
call_button_title: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ChatConfigModel(BaseModel):
|
|
80
|
+
sid: str
|
|
81
|
+
upgrades: List[str]
|
|
82
|
+
pingInterval: PositiveInt
|
|
83
|
+
pingTimeout: PositiveInt
|
|
84
|
+
maxPayload: PositiveInt
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AppConfigUserModel(BaseModel):
|
|
88
|
+
groups_enabled: StrictBool
|
|
89
|
+
photos_enabled: StrictBool
|
|
90
|
+
ios_use_community_portal: StrictBool
|
|
91
|
+
organization_calls_enabled: StrictBool
|
|
92
|
+
ios_learning_center_enabled: StrictBool
|
|
93
|
+
android_use_community_portal: StrictBool
|
|
94
|
+
volunteer_label_call_enabled: StrictBool
|
|
95
|
+
chat_enabled: StrictBool
|
|
96
|
+
sh_scan_qr_code_enabled: StrictBool
|
|
97
|
+
chat_image_jpeg_compression: int
|
|
98
|
+
chat_image_max_dimension: int
|
|
99
|
+
chat_image_type: str
|
|
100
|
+
bmai_speech_output_available: StrictBool
|
|
101
|
+
ios_shortcuts_settings_enabled: StrictBool
|
|
102
|
+
wearable_rbm_blocked: StrictBool
|
|
103
|
+
wearable_rbm_enabled: StrictBool
|
|
104
|
+
wearable_rbm_debug_actions_enabled: StrictBool
|
|
105
|
+
wearable_rbm_promo_enabled: StrictBool
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class WaitingListModel(BaseModel):
|
|
109
|
+
id: int
|
|
110
|
+
created_at: datetime
|
|
111
|
+
modified_at: datetime
|
|
112
|
+
device_type: str
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Image(BaseModel):
|
|
116
|
+
id: int
|
|
117
|
+
created_at: datetime
|
|
118
|
+
upload_finished: StrictBool
|
|
119
|
+
url: HttpUrl
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ChatMessagesModel(BaseModel):
|
|
123
|
+
id: int
|
|
124
|
+
version: PositiveInt
|
|
125
|
+
created_at: datetime
|
|
126
|
+
role: str
|
|
127
|
+
user: Optional[int]
|
|
128
|
+
session: int
|
|
129
|
+
type: str
|
|
130
|
+
error_code: Optional[int]
|
|
131
|
+
data: Optional[str]
|
|
132
|
+
images: List[Image]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
"""example
|
|
136
|
+
m = ChatMessagesModel.model_validate_json(message)
|
|
137
|
+
"""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "bemyai"
|
|
3
|
+
version = "0.1.8"
|
|
4
|
+
description = "Be My AI API"
|
|
5
|
+
authors = ["Alexey <aleks-samos@yandex.ru>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = ">3.8"
|
|
10
|
+
aiohttp = "^3.9.1"
|
|
11
|
+
pydantic = "^2.5.3"
|
|
12
|
+
pillow = "^10.2.0"
|
|
13
|
+
loguru = "^0.7.2"
|
|
14
|
+
aiofiles = "^23.2.1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
[tool.poetry.group.dev.dependencies]
|
|
18
|
+
pytest = "^7.4.4"
|
|
19
|
+
pytest-aiohttp = "^1.0.5"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["poetry-core"]
|
|
23
|
+
build-backend = "poetry.core.masonry.api"
|
|
24
|
+
|
|
25
|
+
[tool.poetry.scripts]
|
|
26
|
+
bm = 'bemyai.__main__:main'
|
|
27
|
+
|