diff --git a/.env.example b/.env.example index 4dcfd8e..53e5ea5 100755 --- a/.env.example +++ b/.env.example @@ -1,3 +1,9 @@ APP_ID= APP_HASH= -TG_PHONE=+15555555555 \ No newline at end of file +TG_PHONE=+15555555555 + +DB_HOST= +DB_USER= +DB_PASS= +DB_PORT=27017 +DB_NAME=fenneko \ No newline at end of file diff --git a/cyber_fenneko/__init__.py b/cyber_fenneko/__init__.py index 2dc545a..152008b 100755 --- a/cyber_fenneko/__init__.py +++ b/cyber_fenneko/__init__.py @@ -1,13 +1,16 @@ import os +import jsonpickle from mongoengine import connect from telemongo import MongoSession -from telethon import TelegramClient +from telethon import Client as TelegramClient from dotenv import load_dotenv from cyber_fenneko.bot import Bot load_dotenv() +jsonpickle.set_encoder_options('json', ensure_ascii=False, indent=4) + # Remember to use your own values from my.telegram.org! api_id = os.environ.get('APP_ID') api_hash = os.environ.get('APP_HASH') @@ -19,9 +22,10 @@ db_user = os.environ.get('DB_USER') db_pass = os.environ.get('DB_PASS') db_name = os.environ.get('DB_NAME') -session = MongoSession(db=db_name, host=db_host, port=int(db_port), username=db_user, password=db_pass) +connect(db=db_name, host=db_host, port=int(db_port), username=db_user, password=db_pass) +session = MongoSession() -client = TelegramClient(session, api_id, api_hash) +client = TelegramClient(session, int(api_id), api_hash) bot = Bot(client) # Import all middlewares from the middleware directory diff --git a/cyber_fenneko/__main__.py b/cyber_fenneko/__main__.py index a2480a0..0c9eb47 100755 --- a/cyber_fenneko/__main__.py +++ b/cyber_fenneko/__main__.py @@ -1,9 +1,30 @@ -from telethon.events import NewMessage, Raw +import asyncio +from datetime import datetime +from telethon.events import NewMessage, Event + +from cyber_fenneko.models.chat import Chat, ChatType from . import client, bot, phone_number -client.add_event_handler(bot._on_raw, Raw) -client.add_event_handler(bot._on_new_message, NewMessage) +client.add_event_handler(bot._on_raw, Event) +# client.add_event_handler(bot._on_new_message, NewMessage) -with client: - client.start(phone_number) - client.run_until_disconnected() \ No newline at end of file +async def main(): + async with client: + me = await client.interactive_login(phone_number) + Chat.objects(id=me.id).update_one( + set__id=me.id, + set__name=me.first_name, + set__username=me.username, + set__chat_type=ChatType.USER, + set__phone=me.phone, + set__packed=me.pack().__bytes__(), + set__updated_at=datetime.utcnow(), + upsert=True + ) + + client.add_event_handler(bot._on_raw, Event) + client.add_event_handler(bot._on_new_message, NewMessage) + + await client.run_until_disconnected() + +asyncio.run(main()) \ No newline at end of file diff --git a/cyber_fenneko/bot.py b/cyber_fenneko/bot.py index e026059..9f166b3 100755 --- a/cyber_fenneko/bot.py +++ b/cyber_fenneko/bot.py @@ -1,17 +1,20 @@ import logging +from pprint import pprint from typing import Dict -from telethon import TelegramClient -from telethon.tl.types import TypeUpdate -from telethon.tl.custom.message import Message +from telethon import Client as TelegramClient +from telethon._impl.client.types import Message +from telethon._impl.tl.abcs import Update +from telethon._impl.client.events import NewMessage, Event from cyber_fenneko.internal.constants import GLOBAL_ARGS from cyber_fenneko.internal.command import Command from cyber_fenneko.internal.command_context import CommandContext from cyber_fenneko.internal.arg_parser import parse_args +from cyber_fenneko.utils import to_dict class Bot: commands: Dict[str, Command] command_aliases: Dict[str, str] - update_handlers: Dict[TypeUpdate, list] + update_handlers: Dict[Update, list] client: TelegramClient logger: logging.Logger default_prefix: str @@ -27,7 +30,7 @@ class Bot: def set_default_prefix(self, prefix: str): self.default_prefix = prefix - def on(self, *events: TypeUpdate): + def on(self, *events: Event): def decorator(func): for event in events: if event not in self.update_handlers: @@ -61,15 +64,17 @@ class Bot: return func # we still want the function to be normally accessible return decorator - async def _on_raw(self, event: TypeUpdate): - if event.__class__ not in self.update_handlers: - return + async def _on_raw(self, event: Update): + pass + # if event.__class__ not in self.update_handlers: + # return - for handler in self.update_handlers[event.__class__]: - await handler(self, event) + # for handler in self.update_handlers[event.__class__]: + # await handler(self, event) - async def _on_new_message(self, event: Message): - message_text = event.message.message.strip() + async def _on_new_message(self, event: NewMessage): + # pprint(to_dict(event)) + message_text = event.text.strip() if not message_text: return @@ -82,9 +87,9 @@ class Bot: if cmd.prefix != prefix: return - if event.out and not cmd.outgoing: + if event._raw.out and not cmd.outgoing: return - elif not event.out and not cmd.incoming: + elif not event._raw.out and not cmd.incoming: return text = message_text[len(command_text) + 1:].strip() @@ -94,16 +99,15 @@ class Bot: await event.respond(f'Error: {e}') return - reply_message = await event.message.get_reply_message() + reply_message = await event.get_replied_message() # create the command context ctx = CommandContext( - client=self.client, bot=self, + client=event.client, raw_text=message_text, text=text, event=event, - message=event.message, reply_to=reply_message, args=args ) @@ -113,4 +117,4 @@ class Bot: # if the command is marked as 'delete', delete the message that triggered it if ctx.args['delete']: - await event.message.delete() + await event.delete() diff --git a/cyber_fenneko/commands/ask.py b/cyber_fenneko/commands/ask.py index 8afc04e..717311c 100755 --- a/cyber_fenneko/commands/ask.py +++ b/cyber_fenneko/commands/ask.py @@ -1,4 +1,3 @@ -import time import openai from cyber_fenneko.internal.constants import OPENAI_API_KEY @@ -29,10 +28,10 @@ async def ask(bot, ctx: CommandContext): # Get the message. If this command was sent in response to a message, use that message. # Otherwise, use the message that invoked the command. - message = ctx.reply_to or ctx.message + message = ctx.reply_to or ctx.event # Get the prompt - prompt = message.message.strip() + prompt = ctx.reply_to.text.strip() if ctx.reply_to else ctx.text.strip() # If this message was sent in response to another message, delete the sent message if ctx.reply_to: @@ -52,7 +51,7 @@ async def ask(bot, ctx: CommandContext): "content": """You are a chatbot within Telegram masquerading as a real person. Your name is watzon, and you should act and respond like watzon would. Watzon is a web developer of 13 years, father of 2, and husband of 1. -You are having a conversation with other humans in a Telegram group chat. +You are having a conversation with other humans in a Telegram group chat. Assume you have been a part of the chat for a while. No need for greetings or introductions. The person you're responding to is either asking a question, or making a statement. You should respond to the human in a way that makes sense. The human is not trying to trick you. You are responding as a user, so act natural. Use slang if necessary, acronyms, etc. @@ -78,4 +77,4 @@ Markdown code blocks should not include the language, and should be delimited by ) # Send the response - await message.reply(completion.choices[0].message.content, link_preview=False, parse_mode='markdown') \ No newline at end of file + await message.reply(markdown=completion.choices[0].message.content, link_preview=False) \ No newline at end of file diff --git a/cyber_fenneko/commands/cache.py b/cyber_fenneko/commands/cache.py index 2fc8474..df24e56 100755 --- a/cyber_fenneko/commands/cache.py +++ b/cyber_fenneko/commands/cache.py @@ -1,21 +1,24 @@ import time - -from cyber_fenneko.utils import format_time -from .. import bot +from telethon._impl.tl.types import ChatParticipant, ChatParticipantCreator, ChatParticipantAdmin +from cyber_fenneko.utils import format_time, to_dict +from .. import bot, Bot from ..internal.command_context import CommandContext @bot.command( 'cache', - description='Cache upto the last 200 dialogs', + description='Cache as many users in the current chat as possible', ) -async def cache(bot, ctx: CommandContext): +async def cache(bot: Bot, ctx: CommandContext): start = time.time() msg = await ctx.event.respond('Caching...') - dlgs = await bot.client.get_dialogs(limit=200) - - end = time.time() - ms = round((end - start) * 1000, 3) - time = format_time(ms) + users = [] + async for participant in ctx.client.get_participants(ctx.event.chat): + id = participant._raw.user_id + user = participant._chat_map[id] + users.append(user) - await msg.edit(f'Cached {len(dlgs)} dialogs ({time})') \ No newline at end of file + print(users) + + end = time.time() + await msg.edit(f'Cached {len(users)} users in {format_time(end - start)}') \ No newline at end of file diff --git a/cyber_fenneko/commands/help.py b/cyber_fenneko/commands/help.py index c5cd087..fde23e9 100755 --- a/cyber_fenneko/commands/help.py +++ b/cyber_fenneko/commands/help.py @@ -9,7 +9,7 @@ async def help(bot, ctx: CommandContext): cmd = ctx.text if cmd: if cmd in bot.commands: - await ctx.respond(bot.commands[cmd].long_help(), parse_mode='Markdown') + await ctx.respond(markdown=bot.commands[cmd].long_help()) else: await ctx.respond(f'Command `{cmd}` not found') else: @@ -19,4 +19,4 @@ async def help(bot, ctx: CommandContext): continue commands.append(command.help()) commands.sort() - await ctx.respond('\n'.join(commands), parse_mode='Markdown') \ No newline at end of file + await ctx.respond(markdown='\n'.join(commands)) \ No newline at end of file diff --git a/cyber_fenneko/commands/highlight.py b/cyber_fenneko/commands/highlight.py index 405db73..65f1648 100755 --- a/cyber_fenneko/commands/highlight.py +++ b/cyber_fenneko/commands/highlight.py @@ -1,9 +1,9 @@ -import time import httpx +from io import BytesIO from tempfile import NamedTemporaryFile from urllib.parse import urlencode from collections import OrderedDict -from telethon.tl.types import TypeMessageEntity, MessageEntityPre, InputMediaPhoto +from telethon._impl.tl.types import MessageEntityPre from cyber_fenneko.models.settings import Settings, HighlightSettings from .. import bot @@ -22,9 +22,9 @@ ENDPOINT_URL = 'https://inkify.0x45.st' Arg('shadow_color', type=ArgType.str, description='The color of the shadow'), Arg('background', type=ArgType.str, description='The background color'), Arg('tab_width', type=ArgType.int, description='The tab width'), - Arg('line_pad', type=ArgType.int, description='The line padding'), - Arg('line_offset', type=ArgType.int, description='The line offset'), - Arg('window_title', type=ArgType.str, description='The window title'), + Arg('line_pad', type=ArgType.int, aliases=['pad'], description='The line padding'), + Arg('line_offset', type=ArgType.int, aliases=['offset'], description='The line offset'), + Arg('window_title', type=ArgType.str, aliases=['title'], description='The window title'), Arg('no_line_number', type=ArgType.bool, description='Whether to hide the line numbers'), Arg('no_round_corner', type=ArgType.bool, description='Whether to round the corners'), Arg('no_window_controls', type=ArgType.bool, description='Whether to hide the window controls'), @@ -46,17 +46,17 @@ async def highlight(bot, ctx: CommandContext): # Check if the user wants to list available themes if ctx.args['themes']: themes = httpx.get(f'{ENDPOINT_URL}/themes').json() - themes_list = ', '.join(themes) - message = f'Available themes ({len(themes)}):\n{themes_list}' - await ctx.event.respond(message) + themes_list = [f'- `{theme}`' for theme in themes] + message = f'Available themes ({len(themes)}):\n' + '\n'.join(themes_list) + await ctx.event.respond(markdown=message) return # Check if the user wants to list available fonts if ctx.args['fonts']: fonts = httpx.get(f'{ENDPOINT_URL}/fonts').json() - fonts_list = ', '.join(fonts) - message = f'Available fonts ({len(fonts)}):\n{fonts_list}' - await ctx.event.respond(message) + fonts_list = [f'- `{font}`' for font in fonts] + message = f'Available fonts ({len(fonts)}):\n' + '\n'.join(fonts_list) + await ctx.event.respond(markdown=message) return # Check if the user wants to list available languages @@ -69,71 +69,82 @@ async def highlight(bot, ctx: CommandContext): # Get the message that triggered this command message = ctx.reply_to - if not message or not message.message: + if not message: await ctx.event.respond('Reply to a message to highlight it') return - # Check if there is a code block in the message - if not message.entities: - await ctx.event.respond('No code block found') - return - - # Filter through entities in the message to find the code block - code_blocks = [] - for entity in message.entities: - if isinstance(entity, MessageEntityPre): - code_blocks.append(message.message[entity.offset:entity.offset + entity.length]) - break - else: - code_blocks.append(ctx.text) + language = ctx.args['language'] + code = None + + # First check if the message contains a file + if message.file: + mime = message.file._mime + if mime and (mime.startswith('text/') or mime.startswith('application/')): + with NamedTemporaryFile(mode='wb', suffix=message.file.ext) as file: + await message.file.download(file) + file.seek(0) + code = file.read().decode('utf-8') + language = language or message.file.ext[1:] + + # Next check if the message contains any code blocks + if not code and message._raw.entities: + for entity in message._raw.entities: + if isinstance(entity, MessageEntityPre): + code = message.text[entity.offset:entity.offset + entity.length] + language = language or entity.language + break + + # If there are no code blocks (or more than one), we'll just use the entire message + if not code: + code = message.text + settings = Settings.objects.first() - print(settings) if settings.highlight is None: settings.highlight = HighlightSettings() settings.save() - for code_block in code_blocks: - # Inkify returns an image, we just need to build the URL - query = OrderedDict( - code=code_block, - language=ctx.args['language'] or settings.highlight.theme, - theme=ctx.args['theme'] or settings.highlight.theme, - font=ctx.args['font'] or settings.highlight.font, - shadow_color=ctx.args['shadow_color'] or settings.highlight.shadow_color, - background=ctx.args['background'] or settings.highlight.background, - tab_width=ctx.args['tab_width'] or settings.highlight.tab_width, - line_pad=ctx.args['line_pad'] or settings.highlight.line_pad, - line_offset=ctx.args['line_offset'] or settings.highlight.line_offset, - window_title=ctx.args['window_title'] or settings.highlight.window_title, - no_line_number=ctx.args['no_line_number'] or settings.highlight.no_line_number, - no_round_corner=ctx.args['no_round_corner'] or settings.highlight.no_round_corner, - no_window_controls=ctx.args['no_window_controls'] or settings.highlight.no_window_controls, - shadow_blur_radius=ctx.args['shadow_blur_radius'] or settings.highlight.shadow_blur_radius, - shadow_offset_x=ctx.args['shadow_offset_x'] or settings.highlight.shadow_offset_x, - shadow_offset_y=ctx.args['shadow_offset_y'] or settings.highlight.shadow_offset_y, - pad_horiz=ctx.args['pad_horiz'] or settings.highlight.pad_horiz, - pad_vert=ctx.args['pad_vert'] or settings.highlight.pad_vert, - highlight_lines=ctx.args['highlight_lines'] or settings.highlight.highlight_lines, - background_image=ctx.args['background_image'] or settings.highlight.background_image, - ) + # Inkify returns an image, we just need to build the URL + query = OrderedDict( + code=code, + language=language, + theme=ctx.args['theme'] or settings.highlight.theme, + font=ctx.args['font'] or settings.highlight.font, + shadow_color=ctx.args['shadow_color'] or settings.highlight.shadow_color, + background=ctx.args['background'] or settings.highlight.background, + tab_width=ctx.args['tab_width'] or settings.highlight.tab_width, + line_pad=ctx.args['line_pad'] or settings.highlight.line_pad, + line_offset=ctx.args['line_offset'] or settings.highlight.line_offset, + window_title=ctx.args['window_title'] or settings.highlight.window_title, + no_line_number=ctx.args['no_line_number'] or settings.highlight.no_line_number, + no_round_corner=ctx.args['no_round_corner'] or settings.highlight.no_round_corner, + no_window_controls=ctx.args['no_window_controls'] or settings.highlight.no_window_controls, + shadow_blur_radius=ctx.args['shadow_blur_radius'] or settings.highlight.shadow_blur_radius, + shadow_offset_x=ctx.args['shadow_offset_x'] or settings.highlight.shadow_offset_x, + shadow_offset_y=ctx.args['shadow_offset_y'] or settings.highlight.shadow_offset_y, + pad_horiz=ctx.args['pad_horiz'] or settings.highlight.pad_horiz, + pad_vert=ctx.args['pad_vert'] or settings.highlight.pad_vert, + highlight_lines=ctx.args['highlight_lines'] or settings.highlight.highlight_lines, + background_image=ctx.args['background_image'] or settings.highlight.background_image, + ) - # Remove any arguments that are None, and transform booleans into lowercase strings - query = {k: v for k, v in query.items() if v is not None} - query = {k: str(v).lower() if isinstance(v, bool) else v for k, v in query.items()} + # Remove any arguments that are None, and transform booleans into lowercase strings + query = {k: v for k, v in query.items() if v is not None} + query = {k: str(v).lower() if isinstance(v, bool) else v for k, v in query.items()} - # Get the image from the URL - url = f'{ENDPOINT_URL}/generate?{urlencode(query)}' - res = httpx.get(url) - - if res.status_code != 200: - await ctx.event.respond(f'Error: {res.status_code}') - return + url = f'{ENDPOINT_URL}/generate?{urlencode(query)}' + async with httpx.AsyncClient() as client: + response = await client.get(url) + file = BytesIO(response.content) - # Save the image to a temporary file - with NamedTemporaryFile(suffix='.png') as f: - f.write(res.content) - f.flush() - - # Send the image - await ctx.event.respond(file=f.name) \ No newline at end of file + try: + await ctx.client.send_photo( + ctx.event.chat, + file=file, + size=int(response.headers['content-length']), + compress=True, + name='highlight.png', + ) + except Exception as e: + await ctx.event.respond(f'Failed to highlight code: {e}') + return \ No newline at end of file diff --git a/cyber_fenneko/commands/jsondump.py b/cyber_fenneko/commands/jsondump.py index ca76173..f69ce09 100755 --- a/cyber_fenneko/commands/jsondump.py +++ b/cyber_fenneko/commands/jsondump.py @@ -1,6 +1,5 @@ -import time -import json -from telethon.tl.types import MessageEntityPre +import jsonpickle +from cyber_fenneko.utils import to_dict from .. import bot from ..internal.command_context import CommandContext from ..internal.arg_parser import Arg, ArgType @@ -17,10 +16,10 @@ from ..utils.paste import paste as paste_util async def jsondump(bot, ctx: CommandContext): # Get the message to paste message = ctx.reply_to or ctx.message - dict = message.to_dict() + dict = to_dict(message) # Prettify the JSON - raw = json.dumps(dict, indent=4, sort_keys=True, default=str) + raw = jsonpickle.encode(dict, unpicklable=False, indent=4) # Check if the JSON is too long if len(raw) > 4000 or ctx.args['paste']: @@ -29,5 +28,5 @@ async def jsondump(bot, ctx: CommandContext): await ctx.event.respond(f'Created paste: {url}', link_preview=False) else: # Respond with the JSON - entities = [MessageEntityPre(0, len(raw), "json")] - await ctx.event.respond(raw, formatting_entities=entities, link_preview=False) + response = f'
{raw}
'
+ await ctx.event.respond(html=response)
diff --git a/cyber_fenneko/commands/paste.py b/cyber_fenneko/commands/paste.py
index edaf928..56204b0 100755
--- a/cyber_fenneko/commands/paste.py
+++ b/cyber_fenneko/commands/paste.py
@@ -1,8 +1,9 @@
import re
+from io import BytesIO
+from tempfile import NamedTemporaryFile
import time
import httpx
-from telethon.tl.types import MessageEntityPre
-from telethon.tl.custom.message import Message
+from telethon._impl.tl.types import MessageEntityPre
from cyber_fenneko.models.paste import Paste
from .. import bot
@@ -28,37 +29,37 @@ ENDPOINT = 'https://0x45.st/api/pastes'
)
async def paste(bot, ctx: CommandContext):
# Get the message to paste
- message: Message = ctx.reply_to
+ message = ctx.reply_to
if message is None:
await ctx.event.respond('You must reply to a message to use this command')
return
- # Get the message text
- text = message.text if ctx.args['raw'] else message.message
- if text is None:
- await ctx.event.respond('You must reply to a text message to use this command')
- return
-
- # Get the language
language = ctx.args['language']
+ code = None
- contents = None
+ # First check if the message contains a file
+ if message.file:
+ mime = message.file._mime
+ if mime and (mime.startswith('text/') or mime.startswith('application/')):
+ buff = BytesIO()
+ await message.file.download(buff)
+ code = buff.getvalue().decode('utf-8')
+ language = language or message.file.ext[1:]
- # Check if the message has entities; any pre entities will be used as the contents
- if message.entities is not None:
- for entity in message.entities:
- if entity.CONSTRUCTOR_ID == MessageEntityPre.CONSTRUCTOR_ID:
- if entity.language is not None:
- language = entity.language
- contents = text[entity.offset:entity.offset + entity.length]
- contents = re.sub(r'```\n?', '', contents)
+ # Next check if the message contains any code blocks
+ if not code and message._raw.entities:
+ for entity in message._raw.entities:
+ if isinstance(entity, MessageEntityPre):
+ code = message.text[entity.offset:entity.offset + entity.length]
+ language = language or entity.language
break
- if not contents:
- contents = text
+ # If there are no code blocks (or more than one), we'll just use the entire message
+ if not code:
+ code = message.text_markdown if ctx.args['raw'] else message.text
- json = paste_util(contents, language)
+ json = paste_util(code, language)
url = json['url']
# Respond with the paste URL
- await ctx.event.respond(f'Created paste: {url}', link_preview=False)
+ await ctx.event.respond(f'Created new paste at:\n{url}', link_preview=False)
diff --git a/cyber_fenneko/commands/search.py b/cyber_fenneko/commands/search.py
new file mode 100644
index 0000000..d9a9fc3
--- /dev/null
+++ b/cyber_fenneko/commands/search.py
@@ -0,0 +1,50 @@
+from urllib import parse
+from cyber_fenneko.internal.arg_parser import Arg, ArgType
+from .. import bot
+from ..internal.command_context import CommandContext
+from cyber_fenneko.models.settings import Settings
+
+engine_map = {
+ 'google': 'https://www.google.com/search?q=%s',
+ 'bing': 'https://www.bing.com/search?q=%s',
+ 'duckduckgo': 'https://duckduckgo.com/?q=%s',
+ 'qwant': 'https://www.qwant.com/?q=%s',
+ 'ecosia': 'https://www.ecosia.org/search?q=%s',
+ 'kagi': 'https://kagi.com/search?q=%s',
+}
+
+@bot.command(
+ 'search',
+ aliases=['google', 'lmgtfy'],
+ args=[
+ # Arg('delete', type=ArgType.bool, default=True),
+ Arg('google', type=ArgType.bool, default=False, description='Use Google as the search engine'),
+ Arg('bing', type=ArgType.bool, default=False, description='Use Bing as the search engine'),
+ Arg('duckduckgo', type=ArgType.bool, aliases=['ddg'], default=False, description='Use DuckDuckGo as the search engine'),
+ Arg('qwant', type=ArgType.bool, default=False, description='Use Qwant as the search engine'),
+ Arg('ecosia', type=ArgType.bool, default=False, description='Use Ecosia as the search engine'),
+ Arg('kagi', type=ArgType.bool, default=False, description='Use Kagi as the search engine'),
+ ],
+ description='"Help people out by posting a link to a search query for their question"',
+)
+async def search(bot, ctx: CommandContext):
+ print(ctx)
+ # Get the search query from the reply message or the sent message
+ query = ctx.reply_to.text if ctx.reply_to else ctx.text
+ if not query:
+ await ctx.event.respond('You must specify a search query')
+ return
+
+ settings = Settings.objects.first()
+
+ # Get the search engine
+ search_engine = None
+ for engine in engine_map:
+ if ctx.args[engine]:
+ search_engine = engine
+
+ search_engine = search_engine or settings.search_engine or 'google'
+ url = engine_map[search_engine] % parse.quote_plus(query)
+
+ # Send the search query
+ await ctx.event.respond(html=f'{query}', link_preview=False)
\ No newline at end of file
diff --git a/cyber_fenneko/commands/user.py b/cyber_fenneko/commands/user.py
index fa39e50..d9c1676 100755
--- a/cyber_fenneko/commands/user.py
+++ b/cyber_fenneko/commands/user.py
@@ -1,112 +1,116 @@
-import time
+# import time
-from telethon.tl.types import User
-from telethon.functions import messages
+# from telethon._impl.tl.types import User
+# from telethon._impl.tl.functions import messages
-from cyber_fenneko.internal.arg_parser import Arg, ArgType
-from cyber_fenneko.utils import format_bool, format_user_status
-from cyber_fenneko.bot import Bot
+# from cyber_fenneko.internal.arg_parser import Arg, ArgType
+# from cyber_fenneko.models.chat import Chat
+# from cyber_fenneko.utils import format_bool, format_user_status
+# from cyber_fenneko.bot import Bot
-from .. import bot
-from ..internal.command_context import CommandContext
+# from .. import bot
+# from ..internal.command_context import CommandContext
-@bot.command(
- 'user',
- aliases=['u'],
- args = [
- Arg('user', type=ArgType.peer, aliases=['u'], description='The user to get information about'),
- Arg('id', type=ArgType.bool, aliases=['i'], default=True, description='Return only the user ID'),
- Arg('mention', type=ArgType.bool, aliases=['m'], default=False, description='Whether to mention the user in the response'),
- Arg('check_forward', type=ArgType.bool, aliases=['fwd'], default=True, description='Whether to check the forward from the reply message'),
+# @bot.command(
+# 'user',
+# aliases=['u'],
+# args = [
+# Arg('user', type=ArgType.peer, aliases=['u'], description='The user to get information about'),
+# Arg('id', type=ArgType.bool, aliases=['i'], default=True, description='Return only the user ID'),
+# Arg('mention', type=ArgType.bool, aliases=['m'], default=False, description='Whether to mention the user in the response'),
+# Arg('check_forward', type=ArgType.bool, aliases=['fwd'], default=True, description='Whether to check the forward from the reply message'),
- Arg('full', type=ArgType.bool, aliases=['f'], default=False, description='Whether to get full information about the user'),
- Arg('general', type=ArgType.bool, aliases=['g'], default=False, description='Whether to show general information about the user'),
- Arg('common_chats', type=ArgType.bool, aliases=['chats'], default=False, description='Whether to get the user\'s common chats'),
- Arg('meta', type=ArgType.bool, default=False, description='Extra infomation about the user that didn\'t fit in the other categories'),
- ],
- description='Gather and return information about the given user',
-)
-async def user(bot: Bot, ctx: CommandContext):
- user: User = ctx.args['user']
-
- if not user:
- # Attempt to get the user from the reply message
- message = ctx.reply_to
- if message:
- try:
- if ctx.args['check_forward'] and message.forward:
- user = await message.forward.get_sender()
- else:
- user = await message.get_sender()
- except:
- await ctx.event.respond('Failed to get user from reply message')
- return
+# Arg('full', type=ArgType.bool, aliases=['f'], default=False, description='Whether to get full information about the user'),
+# Arg('general', type=ArgType.bool, aliases=['g'], default=False, description='Whether to show general information about the user'),
+# Arg('common_chats', type=ArgType.bool, aliases=['chats'], default=False, description='Whether to get the user\'s common chats'),
+# Arg('meta', type=ArgType.bool, default=False, description='Extra infomation about the user that didn\'t fit in the other categories'),
+# ],
+# description='Gather and return information about the given user',
+# )
+# async def user(bot: Bot, ctx: CommandContext):
+# peer: Chat = ctx.args['user']
+# user: User = peer._to_input_peer()
- if not user:
- await ctx.event.respond('You must specify a user to get information about')
- return
+# if not user:
+# # Attempt to get the user from the reply message
+# message = await ctx.event.get_replied_message()
+# if message:
+# try:
+# if ctx.args['check_forward'] and message._raw.fwd_from:
+# # user = message.forward_info
+# # TODO: Forwarded messages
+# raise NotImplementedError('Forwarded messages are not supported yet')
+# else:
+# user = message.sender
+# except:
+# await ctx.event.respond('Failed to get user from reply message')
+# return
- mention = ctx.args['mention']
- show_full = ctx.args['full']
- id = ctx.args['id']
- show_general = show_full or ctx.args['general']
- show_meta = show_full or ctx.args['meta']
- show_common_chats = show_full or ctx.args['common_chats']
+# if not user:
+# await ctx.event.respond('You must specify a user to get information about')
+# return
- if show_general or show_meta or show_common_chats:
- id = False
+# mention = ctx.args['mention']
+# show_full = ctx.args['full']
+# id = ctx.args['id']
+# show_general = show_full or ctx.args['general']
+# show_meta = show_full or ctx.args['meta']
+# show_common_chats = show_full or ctx.args['common_chats']
- # Screen name (first name + last name if available)
- screen_name = user.first_name
- if user.last_name:
- screen_name += f' {user.last_name}'
+# if show_general or show_meta or show_common_chats:
+# id = False
- # Start building a response
- response = ''
- if id:
- if mention:
- response += f'[{screen_name}](tg://user?id={user.id}) (`{user.id}`)'
- else:
- response += f'**{screen_name}**: {user.id}'
- else:
- if mention:
- response += f'[{screen_name}](tg://user?id={user.id}) (`{user.id}`):\n'
- else:
- response += f'**{screen_name}** ({user.id})\n'
+# # Screen name (first name + last name if available)
+# screen_name = user.first_name
+# if user.last_name:
+# screen_name += f' {user.last_name}'
- # General
- if show_general:
- response += f'**general**\n'
- response += f' username: `{user.username}`\n'
- response += f' phone: `{user.phone}`\n'
- response += f' bot: {format_bool(user.bot)}\n'
- response += f' verified: {format_bool(user.verified)}\n'
- response += f' restricted: {format_bool(user.restricted)}\n'
- response += f' support: {format_bool(user.support)}\n'
- response += f' scam: {format_bool(user.scam)}\n'
- response += f' fake: {format_bool(user.fake)}\n'
+# # Start building a response
+# response = ''
+# if id:
+# if mention:
+# response += f'[{screen_name}](tg://user?id={user.id}) (`{user.id}`)'
+# else:
+# response += f'**{screen_name}**: {user.id}'
+# else:
+# if mention:
+# response += f'[{screen_name}](tg://user?id={user.id}) (`{user.id}`):\n'
+# else:
+# response += f'**{screen_name}** ({user.id})\n'
- # Common chats
- if show_common_chats:
- response += f'**common chats**\n'
- common_chats = await bot.client(messages.GetCommonChatsRequest(user.id, 0, 200))
- for chat in common_chats.chats:
- response += f' {chat.title} ({chat.id})\n'
+# # General
+# if show_general:
+# response += f'**general**\n'
+# response += f' username: `{user.username}`\n'
+# response += f' phone: `{user.phone}`\n'
+# response += f' bot: {format_bool(user.bot)}\n'
+# response += f' verified: {format_bool(user.verified)}\n'
+# response += f' restricted: {format_bool(user.restricted)}\n'
+# response += f' support: {format_bool(user.support)}\n'
+# response += f' scam: {format_bool(user.scam)}\n'
+# response += f' fake: {format_bool(user.fake)}\n'
- # Meta
- if show_meta:
- response += f'**meta**\n'
- response += f' access_hash: `{user.access_hash}`\n'
- response += f' bot_chat_history: {format_bool(user.bot_chat_history)}\n'
- response += f' bot_nochats: {format_bool(user.bot_nochats)}\n'
- response += f' bot_inline_geo: {format_bool(user.bot_inline_geo)}\n'
- response += f' min: {format_bool(user.min)}\n'
- response += f' bot_inline_placeholder: {format_bool(user.bot_inline_placeholder)}\n'
- response += f' lang_code: `{user.lang_code}`\n'
- response += f' status: `{format_user_status(user.status)}`\n'
- response += f' bot_info_version: `{user.bot_info_version}`\n'
- response += f' restriction_reason: `{user.restriction_reason}`\n'
- response += f' bot_inline_geo: {format_bool(user.bot_inline_geo)}\n'
+# # Common chats
+# if show_common_chats:
+# response += f'**common chats**\n'
+# common_chats = await bot.client(messages.get_common_chats(user.id, 0, 200))
+# for chat in common_chats.chats:
+# response += f' {chat.title} ({chat.id})\n'
- # Send the response
- await ctx.reply(response, link_preview=False, parse_mode='markdown')
\ No newline at end of file
+# # Meta
+# if show_meta:
+# response += f'**meta**\n'
+# response += f' access_hash: `{user.access_hash}`\n'
+# response += f' bot_chat_history: {format_bool(user.bot_chat_history)}\n'
+# response += f' bot_nochats: {format_bool(user.bot_nochats)}\n'
+# response += f' bot_inline_geo: {format_bool(user.bot_inline_geo)}\n'
+# response += f' min: {format_bool(user.min)}\n'
+# response += f' bot_inline_placeholder: {format_bool(user.bot_inline_placeholder)}\n'
+# response += f' lang_code: `{user.lang_code}`\n'
+# response += f' status: `{format_user_status(user.status)}`\n'
+# response += f' bot_info_version: `{user.bot_info_version}`\n'
+# response += f' restriction_reason: `{user.restriction_reason}`\n'
+# response += f' bot_inline_geo: {format_bool(user.bot_inline_geo)}\n'
+
+# # Send the response
+# await ctx.reply(markdown=response, link_preview=False)
\ No newline at end of file
diff --git a/cyber_fenneko/internal/arg_parser.py b/cyber_fenneko/internal/arg_parser.py
index db5bde7..db043e6 100755
--- a/cyber_fenneko/internal/arg_parser.py
+++ b/cyber_fenneko/internal/arg_parser.py
@@ -2,7 +2,9 @@ import logging
from enum import Enum
from typing import Dict, List, Union
-from telethon import TelegramClient
+from telethon import Client as TelegramClient
+
+from cyber_fenneko.utils import get_chat_by_id, get_chat_by_username
ArgType = Enum('ArgType', ['str', 'int', 'bool', 'enum', 'peer'])
@@ -128,20 +130,21 @@ def parse_args(client: TelegramClient, text: str, cmd_args: List[Arg]) -> (Dict[
# peer is a special type that can be a username, user id, or chat id.
# we need to fetch the user or chat that the id belongs to.
- # Check if the arg value is an int, if so it's a user or chat id
- int_value = None
+ chat = None
try:
int_value = int(arg_value)
+ chat = get_chat_by_id(int_value)
except ValueError:
- pass
+ chat = get_chat_by_username(arg_value)
- id_or_username = int_value if int_value is not None else arg_value
+ if chat is None:
+ raise ValueError(f'Invalid peer value {arg_value} for arg "{arg.name}"')
- try:
- entity = client.get_entity(id_or_username)
- parsed_args[arg.name] = entity
- except ValueError:
- raise ValueError(f'{arg_value} is not a valid user or chat id or username')
+ peer = chat.unpack()
+ if peer is None:
+ raise ValueError(f'Invalid peer value {arg_value} for arg "{arg.name}"')
+
+ parsed_args[arg.name] = peer
elif arg.type == ArgType.bool:
# if the arg is a boolean, we need to check if the value is true or false
if arg_value.lower() == 'true':
diff --git a/cyber_fenneko/internal/command_context.py b/cyber_fenneko/internal/command_context.py
index 1444e31..d598b93 100755
--- a/cyber_fenneko/internal/command_context.py
+++ b/cyber_fenneko/internal/command_context.py
@@ -1,7 +1,7 @@
from typing import Dict, Union
-from telethon import TelegramClient
+from telethon import Client as TelegramClient
from telethon.events import NewMessage
-from telethon.tl.custom.message import Message
+from telethon._impl.client.types import Message
from .. import bot
from .arg_parser import Arg
@@ -26,9 +26,6 @@ class CommandContext:
# The event that triggered this command
event: NewMessage,
- # The message from the event
- message: Message,
-
# The message that was replied to
reply_to: Union[Message, None],
@@ -40,7 +37,6 @@ class CommandContext:
self.raw_text = raw_text
self.text = text
self.event = event
- self.message = message
self.reply_to = reply_to
self.args = args
@@ -49,11 +45,11 @@ class CommandContext:
async def respond(self, *args, **kwargs):
"""Respond to the message that triggered this command."""
- await self.message.respond(*args, **kwargs)
+ await self.event.respond(*args, **kwargs)
async def reply(self, *args, **kwargs):
"""Reply to the message that triggered this command."""
- await self.message.reply(*args, **kwargs)
+ await self.event.reply(*args, **kwargs)
def __repr__(self) -> str:
return f'CommandContext(raw_text={self.raw_text}, text={self.text}, args={self.args})'
\ No newline at end of file
diff --git a/cyber_fenneko/middleware/caching.py b/cyber_fenneko/middleware/caching.py
index fb6106f..36d94ae 100755
--- a/cyber_fenneko/middleware/caching.py
+++ b/cyber_fenneko/middleware/caching.py
@@ -1,26 +1,53 @@
from datetime import datetime
-from telethon.tl.types import User, TypeMessage, MessageEmpty, UpdateNewMessage, UpdateNewChannelMessage, PeerUser, PeerChat, PeerChannel
+from telethon import events
+from telethon._impl.tl.types import User, Chat, Channel
from .. import bot
-from cyber_fenneko.models.entity import Entity
+from cyber_fenneko.models.chat import Chat as Entity, ChatType
-@bot.on(UpdateNewMessage, UpdateNewChannelMessage)
-async def caching(bot, update):
- message: TypeMessage = update.message
- if isinstance(message, MessageEmpty):
- return
+def is_incoming(event):
+ return not event._raw.out
+
+@bot.client.on(events.NewMessage, filter=is_incoming)
+async def caching(event: events.NewMessage):
+ chat = event.sender
+ if chat is not None:
+ ent = Entity.objects(id=chat.id).first() or Entity()
+ ent.id = chat.id
+ ent.name = chat.name
+ ent.username = chat.username
+
+ if isinstance(chat, User):
+ ent.phone = chat.phone
+ ent.chat_type = ChatType.USER
+ elif isinstance(chat, Chat):
+ ent.chat_type = ChatType.GROUP
+ elif isinstance(chat, Channel):
+ ent.chat_type = ChatType.CHANNEL
+
+ if not ent.packed:
+ ent.packed = chat.pack().__bytes__()
+
+ ent.updated_at = datetime.utcnow()
+ ent.save()
- from_id = message.from_id
- if isinstance(from_id, PeerUser):
- # Grab the user from the database
- peer_user = Entity.objects(pk=from_id.user_id).first()
- if peer_user is not None:
- # Only update if it doesn't have a username or hasn't been updated in the last 24 hours
- if (peer_user.username is None) or (peer_user.updated_at is not None and (datetime.utcnow() - peer_user.updated_at).total_seconds() > 86400):
- try:
- # Leave the creation to Telethon, we're just going to update the fields
- user: User = await bot.client.get_entity(from_id.user_id)
- peer_user.updated_at = datetime.utcnow()
- peer_user.save()
- except:
- pass
\ No newline at end of file
+ channel = event.chat
+ if channel is not None:
+ ent = Entity.objects(id=channel.id).first() or Entity()
+ ent.id = channel.id
+ ent.name = channel.name
+ ent.username = channel.username
+
+ if isinstance(chat, User):
+ ent.phone = chat.phone
+ ent.chat_type = ChatType.USER
+ elif isinstance(chat, Chat):
+ ent.chat_type = ChatType.GROUP
+ elif isinstance(chat, Channel):
+ ent.chat_type = ChatType.CHANNEL
+
+ if not ent.packed:
+ ent.packed = channel.pack().__bytes__()
+
+ ent.updated_at = datetime.utcnow()
+ ent.save()
\ No newline at end of file
diff --git a/cyber_fenneko/models/chat.py b/cyber_fenneko/models/chat.py
new file mode 100755
index 0000000..ff59ab9
--- /dev/null
+++ b/cyber_fenneko/models/chat.py
@@ -0,0 +1,30 @@
+from enum import Enum
+from mongoengine import Document, IntField, StringField, DateTimeField, BinaryField, EnumField
+from telethon._impl.session import PackedChat
+
+class ChatType(Enum):
+ USER = 1
+ GROUP = 2
+ CHANNEL = 3
+
+class Chat(Document):
+ id = IntField(primary_key=True)
+ name = StringField()
+ username = StringField()
+ phone = StringField()
+ name = StringField()
+ packed = BinaryField()
+ chat_type = EnumField(ChatType)
+ created_at = DateTimeField()
+ updated_at = DateTimeField(required=True)
+ meta = {
+ 'collection': 'entities',
+ 'indexes': [
+ 'username',
+ 'phone',
+ ]
+ }
+
+ def unpack(self):
+ if self.packed:
+ return PackedChat.from_bytes(self.packed)
\ No newline at end of file
diff --git a/cyber_fenneko/models/entity.py b/cyber_fenneko/models/entity.py
deleted file mode 100755
index e37d091..0000000
--- a/cyber_fenneko/models/entity.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from mongoengine import Document, IntField, StringField, DateTimeField
-
-class Entity(Document):
- id = IntField(primary_key=True)
- hash = IntField(required=True)
- username = StringField()
- phone = StringField()
- name = StringField()
- created_at = DateTimeField()
- updated_at = DateTimeField(required=True)
- meta = {
- 'collection': 'telethon_entities',
- 'indexes': [
- 'username',
- 'phone',
- 'name'
- ]
- }
\ No newline at end of file
diff --git a/cyber_fenneko/models/paste.py b/cyber_fenneko/models/paste.py
index 057a4a5..df64805 100755
--- a/cyber_fenneko/models/paste.py
+++ b/cyber_fenneko/models/paste.py
@@ -1,4 +1,4 @@
-from .entity import Entity
+from .chat import Chat
from mongoengine import Document, StringField, DateTimeField, IntField, BooleanField
from datetime import datetime
@@ -6,7 +6,7 @@ class Paste(Document):
"""Contains a record of a paste creation on Paste69"""
url = StringField(required=True)
language = StringField(required=True)
- chat = Entity()
- author = Entity()
+ chat = Chat()
+ author = Chat()
message_id = IntField()
created_at = DateTimeField(default=datetime.utcnow)
\ No newline at end of file
diff --git a/cyber_fenneko/models/settings.py b/cyber_fenneko/models/settings.py
index 85c1522..9061466 100644
--- a/cyber_fenneko/models/settings.py
+++ b/cyber_fenneko/models/settings.py
@@ -1,25 +1,4 @@
-from .entity import Entity
-from mongoengine import Document, StringField, DateTimeField, IntField, BooleanField, EmbeddedDocument, EmbeddedDocumentField
-from datetime import datetime
-
-# Arg('theme', type=ArgType.str, description='The theme to use for syntax highlighting'),
-# Arg('font', type=ArgType.str, description='The font to use'),
-# Arg('shadow_color', type=ArgType.str, description='The color of the shadow'),
-# Arg('background', type=ArgType.str, description='The background color'),
-# Arg('tab_width', type=ArgType.int, description='The tab width'),
-# Arg('line_pad', type=ArgType.int, description='The line padding'),
-# Arg('line_offset', type=ArgType.int, description='The line offset'),
-# Arg('window_title', type=ArgType.str, description='The window title'),
-# Arg('no_line_number', type=ArgType.bool, description='Whether to hide the line numbers'),
-# Arg('no_round_corner', type=ArgType.bool, description='Whether to round the corners'),
-# Arg('no_window_controls', type=ArgType.bool, description='Whether to hide the window controls'),
-# Arg('shadow_blur_radius', type=ArgType.int, description='The shadow blur radius'),
-# Arg('shadow_offset_x', type=ArgType.int, description='The shadow offset x'),
-# Arg('shadow_offset_y', type=ArgType.int, description='The shadow offset y'),
-# Arg('pad_horiz', type=ArgType.int, default=10, description='The horizontal padding'),
-# Arg('pad_vert', type=ArgType.int, default=10, description='The vertical padding'),
-# Arg('highlight_lines', type=ArgType.str, description='The lines to highlight'),
-# Arg('background_image', type=ArgType.str, description='The background image for the padding area as a URL'),
+from mongoengine import Document, StringField, IntField, BooleanField, EmbeddedDocument, EmbeddedDocumentField
class HighlightSettings(EmbeddedDocument):
"""Stores settings for the highlight command"""
@@ -46,6 +25,7 @@ class HighlightSettings(EmbeddedDocument):
class Settings(Document):
"""Stores configuration settings for the bot"""
highlight = EmbeddedDocumentField(HighlightSettings)
+ search_engine = StringField(default='google')
meta = {
'collection': 'settings'
diff --git a/cyber_fenneko/utils/__init__.py b/cyber_fenneko/utils/__init__.py
index 036d580..4b334e7 100755
--- a/cyber_fenneko/utils/__init__.py
+++ b/cyber_fenneko/utils/__init__.py
@@ -1,4 +1,10 @@
-from telethon.tl.types import TypeUserStatus, UserStatusRecently, UserStatusOnline, UserStatusOffline, UserStatusLastWeek, UserStatusLastMonth, UserStatusEmpty
+import jsonpickle
+from typing import Optional
+from telethon._impl.tl.abcs import UserStatus
+from telethon._impl.tl.core import Serializable
+from telethon._impl.tl.types import UserStatusRecently, UserStatusOnline, UserStatusOffline, UserStatusLastWeek, UserStatusLastMonth, UserStatusEmpty
+
+from cyber_fenneko.models.chat import Chat
# Format time (in ms) to a human readable format.
def format_time(ms: float) -> str:
@@ -23,7 +29,7 @@ def format_bool(value: bool) -> str:
return 'Yes' if value else 'No'
# Convert a TypeUserStatus to a string.
-def format_user_status(status: TypeUserStatus) -> str:
+def format_user_status(status: UserStatus) -> str:
if isinstance(status, UserStatusRecently):
return 'Recently'
elif isinstance(status, UserStatusOnline):
@@ -37,4 +43,23 @@ def format_user_status(status: TypeUserStatus) -> str:
elif isinstance(status, UserStatusEmpty):
return 'Empty'
else:
- return 'Unknown'
\ No newline at end of file
+ return 'Unknown'
+
+def to_dict(obj: object) -> dict:
+ if obj is None or isinstance(obj, (bool, int, bytes, str)): return obj
+ if isinstance(obj, (list, tuple)): return [to_dict(v) for v in obj]
+ if isinstance(obj, dict): return {k: to_dict(v) for k, v in obj.items()}
+ if hasattr(obj, '_from_raw') or isinstance(obj, Serializable):
+ props = dir(obj)
+ return {k: to_dict(getattr(obj, k)) for k in props if not k.startswith('_') and not callable(getattr(obj, k))}
+ return obj.__repr__()
+
+def get_chat_by_id(id: int) -> Optional[Chat]:
+ return Chat.objects(id=id).first()
+
+def get_chat_by_username(username: str) -> Optional[Chat]:
+ username = username.lstrip('@')
+ return Chat.objects(username=username).first()
+
+def get_chat_by_phone(phone: str) -> Optional[Chat]:
+ return Chat.objects(phone=phone).first()
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index f30da0d..a670e42 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -96,39 +96,40 @@ files = [
[[package]]
name = "httpcore"
-version = "0.18.0"
+version = "1.0.1"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
files = [
- {file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"},
- {file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"},
+ {file = "httpcore-1.0.1-py3-none-any.whl", hash = "sha256:c5e97ef177dca2023d0b9aad98e49507ef5423e9f1d94ffe2cfe250aa28e63b0"},
+ {file = "httpcore-1.0.1.tar.gz", hash = "sha256:fce1ddf9b606cfb98132ab58865c3728c52c8e4c3c46e2aabb3674464a186e92"},
]
[package.dependencies]
-anyio = ">=3.0,<5.0"
certifi = "*"
h11 = ">=0.13,<0.15"
-sniffio = "==1.*"
[package.extras]
+asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
+trio = ["trio (>=0.22.0,<0.23.0)"]
[[package]]
name = "httpx"
-version = "0.25.0"
+version = "0.25.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
files = [
- {file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"},
- {file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"},
+ {file = "httpx-0.25.1-py3-none-any.whl", hash = "sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a"},
+ {file = "httpx-0.25.1.tar.gz", hash = "sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"},
]
[package.dependencies]
+anyio = "*"
certifi = "*"
-httpcore = ">=0.18.0,<0.19.0"
+httpcore = "*"
idna = "*"
sniffio = "*"
@@ -149,6 +150,22 @@ files = [
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
+[[package]]
+name = "jsonpickle"
+version = "3.0.2"
+description = "Python library for serializing any arbitrary object graph into JSON"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jsonpickle-3.0.2-py3-none-any.whl", hash = "sha256:4a8442d97ca3f77978afa58068768dba7bff2dbabe79a9647bc3cdafd4ef019f"},
+ {file = "jsonpickle-3.0.2.tar.gz", hash = "sha256:e37abba4bfb3ca4a4647d28bb9f4706436f7b46c8a8333b4a718abafa8e46b37"},
+]
+
+[package.extras]
+docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"]
+testing = ["ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"]
+testing-libs = ["simplejson", "ujson"]
+
[[package]]
name = "markdown-it-py"
version = "3.0.0"
@@ -200,13 +217,13 @@ pymongo = ">=3.4,<5.0"
[[package]]
name = "openai"
-version = "1.0.0rc1"
+version = "1.1.1"
description = "Client library for the openai API"
optional = false
python-versions = ">=3.7.1"
files = [
- {file = "openai-1.0.0rc1-py3-none-any.whl", hash = "sha256:a2075ef79acdae200798999b33782e99a48a64fd6ca61f7691d5b25a179fdc1c"},
- {file = "openai-1.0.0rc1.tar.gz", hash = "sha256:f082b0158529f5d826155b065493994bcdfd66d46045805668a4ad57fdfc0b24"},
+ {file = "openai-1.1.1-py3-none-any.whl", hash = "sha256:1496418b132c88352bcfffa8c24e83a69f0e01b1484cbb7bb48f722aad8fd6e1"},
+ {file = "openai-1.1.1.tar.gz", hash = "sha256:80e49cb21d8445f6d51339b8af7376fc83302c78ab78578b78133ef89634869d"},
]
[package.dependencies]
@@ -380,92 +397,92 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pymongo"
-version = "4.5.0"
+version = "4.6.0"
description = "Python driver for MongoDB