├── simsun.ttf ├── telefire.py ├── requirements.txt ├── Dockerfile ├── plugins ├── get_all_chats.py ├── __init__.py ├── get_entity.py ├── log_chat.py ├── get_messages_by_ids.py ├── words_to_ifttt.py ├── special_attention_mode.py ├── auto_reply_test.py ├── words_to_pushbullet.py ├── delete_all.py ├── list_messages.py ├── search_messages.py ├── word_cloud.py ├── auto_invite.py ├── base.py ├── plus_mode.py └── word_cloud_inchat.py ├── LICENSE ├── utils.py ├── .gitignore └── README.md /simsun.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeiShi1313/TeleFire/HEAD/simsun.ttf -------------------------------------------------------------------------------- /telefire.py: -------------------------------------------------------------------------------- 1 | import fire 2 | from plugins.base import Commands 3 | 4 | 5 | if __name__ == '__main__': 6 | fire.Fire(Commands) 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.7.4 2 | fire==0.3.1 3 | Telethon==1.11.3 4 | python-dateutil==2.8.1 5 | aioredis==1.3.1 6 | wordcloud==1.7.0 7 | jieba==0.42.1 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | RUN apt-get update && apt-get install gcc -y && apt-get clean 4 | 5 | RUN mkdir -p /tg 6 | WORKDIR /tg 7 | 8 | COPY requirements.txt requirements.txt 9 | RUN pip install -r requirements.txt 10 | 11 | -------------------------------------------------------------------------------- /plugins/get_all_chats.py: -------------------------------------------------------------------------------- 1 | from telethon.sync import events 2 | from plugins.base import Telegram, PluginMount 3 | 4 | 5 | class GetAllChats(Telegram, metaclass=PluginMount): 6 | command_name = 'get_all_chats' 7 | 8 | def __call__(self): 9 | async def _get_all_chats(self): 10 | async for dialog in self._client.iter_dialogs(): 11 | self._logger.info('{:>14}: {}'.format(dialog.id, dialog.title)) 12 | 13 | with self._client: 14 | self._client.loop.run_until_complete(_get_all_chats(self)) 15 | 16 | -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import inspect 3 | import pkgutil 4 | 5 | 6 | class Wrapper: 7 | def __getattr__(self, item): 8 | return globals().get(item, None) 9 | 10 | 11 | __all__ = [] 12 | for loader, name, is_pkg in pkgutil.walk_packages(__path__): 13 | module = loader.find_module(name).load_module(name) 14 | for name, value in inspect.getmembers(module): 15 | if name.startswith('__'): 16 | continue 17 | globals()[name] = value 18 | __all__.append(name) 19 | __all__.append(module) 20 | sys.modules[__name__] = Wrapper() 21 | -------------------------------------------------------------------------------- /plugins/get_entity.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pickle 3 | import traceback 4 | from telethon import utils 5 | from telethon.tl.functions.channels import CreateChannelRequest 6 | from telethon.tl.types import TypeMessagesFilter 7 | 8 | from plugins.base import Telegram, PluginMount 9 | 10 | 11 | class Action(Telegram, metaclass=PluginMount): 12 | command_name = 'get_entity' 13 | 14 | async def _get_entity_async(self, entity): 15 | print(await self._get_entity(entity)) 16 | 17 | def __call__(self, entity): 18 | with self._client: 19 | self._client.loop.run_until_complete( 20 | self._get_entity_async(entity)) 21 | -------------------------------------------------------------------------------- /plugins/log_chat.py: -------------------------------------------------------------------------------- 1 | from telethon.sync import events 2 | from plugins.base import Telegram, PluginMount 3 | 4 | 5 | class LogChat(Telegram, metaclass=PluginMount): 6 | command_name = 'log_chat' 7 | 8 | def __call__(self, chat): 9 | @self._client.on(events.NewMessage) 10 | async def _inner(evt): 11 | msg = evt.message 12 | channel = await self._client.get_entity(msg.to_id) 13 | if channel.username == chat: 14 | if msg.media: 15 | self._logger.info(msg) 16 | 17 | self._set_file_handler('log_chat') 18 | self._client.start() 19 | self._client.run_until_disconnected() 20 | -------------------------------------------------------------------------------- /plugins/get_messages_by_ids.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pickle 3 | import traceback 4 | from telethon import utils 5 | from telethon.tl.functions.channels import CreateChannelRequest 6 | from telethon.tl.types import TypeMessagesFilter 7 | 8 | from plugins.base import Telegram, PluginMount 9 | 10 | 11 | class Action(Telegram, metaclass=PluginMount): 12 | command_name = 'get_messages_by_ids' 13 | 14 | async def _get_message_by_ids_async(self, chat, ids): 15 | async for msg in self._client.iter_messages(chat, ids=ids): 16 | print(msg) 17 | 18 | def __call__(self, chat, *ids): 19 | with self._client: 20 | self._client.loop.run_until_complete( 21 | self._get_message_by_ids_async(chat, ids)) 22 | -------------------------------------------------------------------------------- /plugins/words_to_ifttt.py: -------------------------------------------------------------------------------- 1 | from telethon.sync import events 2 | from plugins.base import Telegram, PluginMount 3 | from utils import get_url 4 | 5 | 6 | class WordsToIfttt(Telegram, metaclass=PluginMount): 7 | command_name = 'words_to_ifttt' 8 | 9 | def __call__(self, event, key, *words): 10 | @self._client.on(events.NewMessage) 11 | async def _inner(evt): 12 | if any(w.lower() in evt.raw_text.lower() for w in words): 13 | msg = evt.message 14 | channel = await self._client.get_entity(msg.to_id) 15 | header = "{}在{}说了: ".format(' '.join([msg.sender.first_name, msg.sender.last_name]),channel.title) 16 | body = evt.raw_text[:20] + ('...' if len(evt.raw_text) > 20 else '') 17 | url = get_url(channel, msg) 18 | await self._send_to_ifttt_async(event, key, header, body, url) 19 | 20 | self._set_file_handler('words_to_ifttt') 21 | self._logger.info("Sending messages to IFTTT for words:{}".format(words)) 22 | self._client.start() 23 | self._client.run_until_disconnected() 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Lei Shi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plugins/special_attention_mode.py: -------------------------------------------------------------------------------- 1 | from telethon.sync import events 2 | 3 | from plugins.base import Telegram, PluginMount 4 | from utils import get_url 5 | 6 | 7 | class SpecialAttentionMode(Telegram, metaclass=PluginMount): 8 | command_name = 'special_attention_mode' 9 | 10 | def __call__(self, event, key, *people): 11 | @self._client.on(events.NewMessage) 12 | async def _inner(evt): 13 | msg = evt.message 14 | sender = evt.message.sender 15 | if sender and any(sender.id==p or sender.username==p for p in people): 16 | channel = await self._client.get_entity(msg.to_id) 17 | header = "{}在{}说了: ".format(' '.join([msg.sender.first_name, msg.sender.last_name]),channel.title) 18 | body = evt.raw_text[:20] + ('...' if len(evt.raw_text) > 20 else '') 19 | url = get_url(channel, msg) 20 | await self._send_to_ifttt_async(event, key, header, body, url) 21 | 22 | self._set_file_handler('special_attention_mode') 23 | self._logger.info("Sending messages to IFTTT for people:{}".format(people)) 24 | self._client.start() 25 | self._client.run_until_disconnected() 26 | -------------------------------------------------------------------------------- /plugins/auto_reply_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import random 4 | import traceback 5 | from collections import defaultdict 6 | from telethon import utils as telethon_utils 7 | from telethon.sync import events 8 | from plugins.base import Telegram, PluginMount 9 | from utils import get_url 10 | 11 | 12 | class Action(Telegram, metaclass=PluginMount): 13 | command_name = "auto_reply_test" 14 | 15 | def __call__(self, chat, reply): 16 | import aioredis 17 | @self._client.on(events.NewMessage) 18 | async def _inner(evt): 19 | msg = evt.message 20 | try: 21 | to_chat = await evt.get_chat() 22 | sender = await msg.get_sender() 23 | if not msg.is_reply and to_chat.username == chat or to_chat.id == chat: 24 | self._log_message(msg, to_chat, sender) 25 | await evt.reply(reply) 26 | except Exception as e: 27 | traceback.print_exc() 28 | 29 | 30 | self._set_file_handler("auto_reply_test") 31 | self._logger.info(f"Auto reply start for chat {chat}, reply: {reply}") 32 | self._client.start() 33 | self._client.run_until_disconnected() 34 | -------------------------------------------------------------------------------- /plugins/words_to_pushbullet.py: -------------------------------------------------------------------------------- 1 | from telethon.sync import events 2 | from plugins.base import Telegram, PluginMount 3 | from utils import get_url, send_to_pushbullet 4 | 5 | 6 | class WordsToPushbullet(Telegram, metaclass=PluginMount): 7 | command_name = 'words_to_pushbullet' 8 | 9 | def __call__(self, token, device, *words): 10 | @self._client.on(events.NewMessage) 11 | async def _inner(evt): 12 | if any(w.lower() in evt.raw_text.lower() for w in words): 13 | msg = evt.message 14 | channel = await self._client.get_entity(msg.to_id) 15 | header = "{}在{}说了: ".format(' '.join([msg.sender.first_name, msg.sender.last_name]),channel.title) 16 | body = evt.raw_text[:20] + ('...' if len(evt.raw_text) > 20 else '') 17 | url = get_url(channel, msg) 18 | status, resp_text = await send_to_pushbullet(token, device, header, body, url) 19 | if status == 200: 20 | self._logger.info("[{}] {}{}\nPushbullet status: [{}]".format(url, header, body, status, resp_text)) 21 | else: 22 | self._logger.info("[{}] {}{}\nPushbullet status: [{}] {}".format(url, header, body, status, resp_text)) 23 | 24 | self._set_file_handler('words_to_pushbullet') 25 | self._logger.info("Sending messages to Pushbullet for words:{}".format(words)) 26 | self._client.start() 27 | self._client.run_until_disconnected() 28 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import aiohttp 3 | from telethon.tl.types import Channel, Message 4 | 5 | 6 | def camel_to_snake(name: str): 7 | name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 8 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() 9 | 10 | 11 | def get_url(channel: Channel, msg: Message): 12 | if channel.username: 13 | return "https://t.me/{}/{}".format(channel.username, msg.id) 14 | else: 15 | return "https://t.me/c/{}/{}".format(channel.id, msg.id) 16 | 17 | 18 | async def _send_to_ifttt_async(event: str, key: str, header, body, url): 19 | payload = {'value1': header, 'value2': body, 'value3': url} 20 | u = 'https://maker.ifttt.com/trigger/{}/with/key/{}'.format(event, key) 21 | async with aiohttp.ClientSession() as session: 22 | async with session.post(u, data=payload) as resp: 23 | self._logger.info("[{}] {}{}\nIFTTT status: {}".format(url, header, body, resp.status)) 24 | 25 | async def send_to_pushbullet(access_token: str, device_iden: str, title: str, body: str, url: str): 26 | payload: dict = { 27 | 'title': title, 28 | 'body': body, 29 | 'url': url, 30 | 'type': 'note', 31 | 'device_iden': device_iden 32 | } 33 | headers = {'Access-Token': access_token} 34 | async with aiohttp.ClientSession() as session: 35 | async with session.post('https://api.pushbullet.com/v2/pushes', json=payload, headers=headers) as resp: 36 | return resp.status, await resp.text() -------------------------------------------------------------------------------- /plugins/delete_all.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from datetime import timezone 3 | from dateutil import parser 4 | from telethon import utils 5 | from plugins.base import Telegram, PluginMount 6 | 7 | 8 | class DeleteAll(Telegram, metaclass=PluginMount): 9 | command_name = 'delete_all' 10 | 11 | async def _delete_all_async(self, chat: str, before: str, query: str) -> None: 12 | user = await self._client.get_me() 13 | channel = await self._client.get_entity(chat) 14 | 15 | self._set_file_handler('delete_all', channel, user) 16 | self._logger.info("Deleting messages for {} in {}".format( 17 | utils.get_display_name(user), channel.title)) 18 | async for msg in self._client.iter_messages(channel, from_user=user): 19 | should_delete = True 20 | if before is not None: 21 | try: 22 | before_date = parser.parse(str(before)).replace(tzinfo=timezone.utc) 23 | should_delete = msg.date <= before_date 24 | except Exception as e: 25 | self._logger.warning(traceback.format_exc()) 26 | elif query is not None: 27 | should_delete = msg.text and query in msg.text 28 | if should_delete: 29 | self._log_message(msg, channel, user) 30 | await msg.delete() 31 | 32 | 33 | def __call__(self, chat: str, before=None, query=None) -> None: 34 | with self._client: 35 | self._client.loop.run_until_complete(self._delete_all_async(chat, before, query)) 36 | -------------------------------------------------------------------------------- /plugins/list_messages.py: -------------------------------------------------------------------------------- 1 | from telethon import utils 2 | from telethon.tl.functions.channels import CreateChannelRequest 3 | 4 | from plugins.base import Telegram, PluginMount 5 | 6 | 7 | class ListMessages(Telegram, metaclass=PluginMount): 8 | command_name = 'list_messages' 9 | 10 | async def _list_messages_async(self, chat, user, output, print_stat=False): 11 | channel = await self._client.get_entity(chat) 12 | if user is not None: 13 | try: 14 | user = await self._client.get_entity(user) 15 | except ValueError as e: 16 | if user.isdecimal(): 17 | user = await self._client.get_entity(int(user)) 18 | else: 19 | raise e 20 | 21 | if output == 'channel': 22 | result = await self._client(CreateChannelRequest( 23 | "List Messages", 24 | "Messages For {} in {}".format(utils.get_display_name(user), channel.title))) 25 | created_channel = result.chats[0] 26 | self._logger.info("Channel: {} created.".format(created_channel.title)) 27 | await self._iter_messages_async(channel, user, '', created_channel, print_stat) 28 | else: 29 | self._set_file_handler('list_messages', channel, user) 30 | 31 | self._logger.debug(channel) 32 | self._logger.debug(user) 33 | await self._iter_messages_async(channel, user, '', 'log', print_stat) 34 | self._logger.info("Listing all messages {}in {}".format( 35 | 'for {} '.format(utils.get_display_name(user)) if user else '', channel.title)) 36 | 37 | def __call__(self, chat, user=None, output='log', print_stat=False): 38 | with self._client: 39 | self._client.loop.run_until_complete( 40 | self._list_messages_async(chat, user, output, print_stat)) 41 | -------------------------------------------------------------------------------- /plugins/search_messages.py: -------------------------------------------------------------------------------- 1 | from telethon.tl.functions.messages import SearchRequest 2 | from telethon.tl.functions.channels import CreateChannelRequest 3 | from telethon.tl.types import InputMessagesFilterEmpty 4 | 5 | from plugins.base import Telegram, PluginMount 6 | 7 | 8 | class SearchMessages(Telegram, metaclass=PluginMount): 9 | command_name = 'search_messages' 10 | 11 | async def _search_messages_async(self, chat, query, slow, limit, user, output): 12 | _filter = InputMessagesFilterEmpty() 13 | peer = await self._client.get_entity(chat) 14 | if user is not None: 15 | user = await self._client.get_entity(user) 16 | 17 | self._set_file_handler('search_messages', peer, user, query) 18 | 19 | if slow: 20 | if output == 'channel': 21 | result = await self._client(CreateChannelRequest( 22 | "Search Messages", 23 | "Messages in {}".format(peer.title))) 24 | output = result.chats[0] 25 | self._logger.info("Channel: {} created.".format(output.title)) 26 | await self._iter_messages_async(peer, user, query, output) 27 | else: 28 | search_request = SearchRequest( 29 | peer=peer, 30 | q=query, 31 | filter=_filter, 32 | min_date=None, 33 | max_date=None, 34 | offset_id=0, 35 | add_offset=0, 36 | limit=limit, 37 | max_id=0, 38 | min_id=0, 39 | hash=0, 40 | from_id=user) 41 | result = await self._client(search_request) 42 | for msg in result.messages: 43 | sender = user 44 | if sender is None: 45 | sender = await self._client.get_entity(msg.from_id) 46 | self._log_message(msg, peer, sender) 47 | 48 | def __call__(self, chat, query, slow=False, limit=100, user=None, output='log'): 49 | with self._client: 50 | self._client.loop.run_until_complete( 51 | self._search_messages_async(chat, query, slow, limit, user, output)) 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Telethon session 132 | *.session 133 | *.session-journal 134 | 135 | # Vim stuff 136 | *.swp 137 | 138 | # Intellij 139 | .idea/ 140 | 141 | # VS Code 142 | .vscode/ 143 | 144 | *.pkl -------------------------------------------------------------------------------- /plugins/word_cloud.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from datetime import timezone 3 | from dateutil import parser 4 | from telethon import utils 5 | from telethon.tl.functions.channels import CreateChannelRequest 6 | 7 | from plugins.base import Telegram, PluginMount 8 | 9 | stop_words = {"www", "com", "https", "http", "htm", "html", "还是", "就是", "这是", "以前", "不过", "不行", "这个", "那个", "什么", "感觉", "现在", "可以", "不是", "知道", "好像", "但是", "还有", "怎么", "然后", "你们", "我们", "时候", "没有", "自己", "一个", "这么", "觉得", "而且", "这种", "已经", "不到", "开始", "很多", "这样", "可能", "有点", "之前", "以后", "最近", "所以", "应该", "里面", "不会", "出来", "真的", "只有", "地方", "真的", "看到", "不了", "那么", "不用", "主要", "反正", "其实", "的话", "起来", "肯定", "如果", "只是", "问题", "事情", "估计", "直接", "因为", "不能", "需要", "一样", "确实", "不然", "估计", "发现", "大家", "今天", "或者", "这边", "为了", "本来", "东西", "是不是", "不要", "基本", "一般", "多少", "以下", "一下", "有些", "有人", "他们", "这次", "其他", "只能", "这些", "看看", "那种"} 10 | 11 | class Action(Telegram, metaclass=PluginMount): 12 | command_name = "word_cloud" 13 | 14 | async def _generate_word_cloud_async(self, chat, user, start, end): 15 | try: 16 | import jieba 17 | from wordcloud import WordCloud 18 | except ImportError as e: 19 | print(e) 20 | return 21 | 22 | chat = await self._get_entity(chat) 23 | if user is not None: 24 | user = await self._get_entity(user) 25 | if start is not None: 26 | start = parser.parse(start).replace(tzinfo=timezone.utc) 27 | if end is not None: 28 | end = parser.parse(end) 29 | words = [] 30 | async for msg in self._client.iter_messages(chat, from_user=user, offset_date=end): 31 | if start and msg.date < start: 32 | break 33 | if msg.text: 34 | print("[{}][{}] {}".format( 35 | msg.date, 36 | utils.get_display_name(await msg.get_sender()) if user is None else utils.get_display_name(user), 37 | msg.text)) 38 | words += [w for w in jieba.cut_for_search(msg.text) if w not in stop_words] 39 | image = WordCloud(font_path="simsun.ttf", width=800, height=400).generate(' '.join(words)).to_image() 40 | stream = BytesIO() 41 | image.save(stream, 'PNG') 42 | await self._client.send_message( 43 | 'gua_mei_debug', 44 | '{}{}{}'.format( 45 | f'{chat.title}', 46 | f'\n{utils.get_display_name(user)}' if user else '', 47 | '\n{}-{}'.format( 48 | start.strftime('%Y/%m/%d') if start else 'Join', 49 | end.strftime('%Y/%m/%d') if end else 'Now') if start or end else ''), 50 | file=stream.getvalue()) 51 | 52 | 53 | def __call__(self, chat: str, user=None, start=None, end=None): 54 | with self._client: 55 | self._client.loop.run_until_complete( 56 | self._generate_word_cloud_async(chat, user, start, end)) 57 | -------------------------------------------------------------------------------- /plugins/auto_invite.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pickle 3 | import random 4 | import traceback 5 | from collections import defaultdict 6 | from telethon import utils as telethon_utils 7 | from telethon.sync import events 8 | from plugins.base import Telegram, PluginMount 9 | from utils import get_url 10 | 11 | 12 | class Action(Telegram, metaclass=PluginMount): 13 | command_name = "auto_invite" 14 | prefix = "auto_invite_" 15 | 16 | async def incr_attempts(self, sender): 17 | return await self.redis.hincrby(f'{self.prefix}attempts', sender.id) 18 | 19 | async def get_one_reply(self, key): 20 | return (await self.redis.srandmember(f'{self.prefix}{key}', 1, encoding='utf-8'))[0] 21 | 22 | async def get_reply(self): 23 | codes = await self.redis.hgetall(f'{self.prefix}codes', encoding='utf-8') 24 | codes = {k: sum(int(_v) for _v in codes.values()) - int(v) for k,v in codes.items()} 25 | return "也可以从群友的这些码里选择:\n{}".format(', '.join( 26 | map(lambda c: f'`{c}`', random.choices(list(codes.keys()), list(codes.values()), k=5)))) 27 | 28 | async def not_code(self, code): 29 | return all('a' <= c <= 'z' for c in code) or all('A' <= c <= 'Z' for c in code) or all('0' <= c <= '9' for c in code) or await self.redis.sismember(f'{self.prefix}not_codes', code) 30 | 31 | async def is_asking_code(self, msg, to_chat, sender): 32 | if not msg.is_reply and msg.text and any([val.decode('utf-8') in msg.text.lower() async for val in self.redis.isscan(f'{self.prefix}words')]): 33 | if (to_chat.username and await self.redis.sismember(f'{self.prefix}chats', to_chat.username)) \ 34 | or (to_chat.id and await self.redis.sismember(f'{self.prefix}chats', to_chat.id)): 35 | self._log_message(msg, to_chat, sender) 36 | return True 37 | return False 38 | 39 | async def is_in_chat(self, to_chat): 40 | return (to_chat.username and await self.redis.sismember(f'{self.prefix}chats', to_chat.username)) \ 41 | or (to_chat.id and await self.redis.sismember(f'{self.prefix}chats', to_chat.id)) 42 | 43 | def __call__(self, redis, prefix = None): 44 | if prefix is not None: 45 | self.prefix = prefix 46 | import aioredis 47 | regex = re.compile(r'^([a-zA-Z0-9]{4})$|^.*邀请码.*([a-zA-Z0-9]{4}).*$|^.*code=([a-zA-Z0-9]{4}).*$', re.MULTILINE) 48 | @self._client.on(events.NewMessage) 49 | async def _inner(evt): 50 | msg = evt.message 51 | try: 52 | to_chat = await evt.get_chat() 53 | if not await self.is_in_chat(to_chat): 54 | return 55 | 56 | sender = await evt.get_sender() 57 | m = regex.match(msg.text) 58 | if msg.text and m: 59 | for code in m.groups(): 60 | if code and not await self.not_code(code): 61 | self._log_message(msg, to_chat, sender) 62 | self._logger.info(f'{telethon_utils.get_display_name(sender)}({sender.id}) sent a code: {code}') 63 | await self.incr_attempts(sender) 64 | await self.redis.hincrby('auto_invite_codes', code) 65 | return 66 | if await self.is_asking_code(msg, to_chat, sender): 67 | attempts = await self.incr_attempts(sender) 68 | if attempts == 1 or sender.id == self.me.id: 69 | one_reply = await self.get_one_reply('replies') 70 | await msg.reply(one_reply) 71 | else: 72 | self._logger.info(f'{telethon_utils.get_display_name(sender)}({sender.id}) in {to_chat.username} sent {msg.text}, attempts: {attempts}') 73 | except Exception as e: 74 | traceback.print_exc() 75 | 76 | async def connect_redis(): 77 | self.me = await self._client.get_me() 78 | self.redis = await aioredis.create_redis_pool(f'redis://{redis}') 79 | 80 | with self._client: 81 | self._client.loop.run_until_complete(connect_redis()) 82 | self._set_file_handler("auto_invite") 83 | self._logger.info("Auto invite start") 84 | self._client.start() 85 | self._client.run_until_disconnected() 86 | -------------------------------------------------------------------------------- /plugins/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import logging 5 | from math import floor 6 | from collections import Counter 7 | 8 | import aiohttp 9 | from telethon import utils 10 | from telethon.sync import TelegramClient 11 | from telethon.tl.types import Channel, Message, User 12 | 13 | from utils import get_url, camel_to_snake 14 | 15 | 16 | class Telegram(object): 17 | def __init__(self, session='test', log_level='info'): 18 | api_id = os.environ.get("TELEGRAM_API_ID") 19 | api_hash = os.environ.get("TELEGRAM_API_HASH") 20 | if not api_id or not api_hash: 21 | raise ValueError("Please set TELEGRAM_API_ID and TELEGRAM_API_HASH as environment variables!") 22 | self._client = TelegramClient(session, api_id, api_hash) 23 | 24 | self._logger = logging.getLogger(__name__) 25 | self._logger.setLevel(logging.INFO if log_level=='info' else logging.DEBUG) 26 | # logFormatter = logging.Formatter("%(asctime)s [%(name)s] [%(levelname)s] %(message)s") 27 | self._logFormatter = logging.Formatter("%(message)s") 28 | console_handler = logging.StreamHandler(sys.stdout) 29 | console_handler.setFormatter(self._logFormatter) 30 | self._logger.addHandler(console_handler) 31 | 32 | def _set_file_handler(self, method, channel=None, user=None, query=None): 33 | file_handler = logging.FileHandler( 34 | "{}{}{}{}.log".format( 35 | method, 36 | '_[{}]'.format(channel.title) if channel else '', 37 | '_[{}]'.format(utils.get_display_name(user)) if user else '', 38 | '_[query={}]'.format(query) if query else '')) 39 | file_handler.setFormatter(self._logFormatter) 40 | self._logger.addHandler(file_handler) 41 | 42 | def _log_message(self, msg: Message, channel: Channel, user: User): 43 | self._logger.info("{} [{}] [{}]: {}".format( 44 | msg.date, 45 | get_url(channel, msg), 46 | utils.get_display_name(user), 47 | msg.text)) 48 | 49 | async def _send_to_ifttt_async(self, event, key, header, body, url): 50 | payload = {'value1': header, 'value2': body, 'value3': url} 51 | u = 'https://maker.ifttt.com/trigger/{}/with/key/{}'.format(event, key) 52 | async with aiohttp.ClientSession() as session: 53 | async with session.post(u, data=payload) as resp: 54 | self._logger.info("[{}] {}{}\nIFTTT status: {}".format(url, header, body, resp.status)) 55 | 56 | async def _iter_messages_async(self, chat, user, query, output, print_stat=False): 57 | if print_stat: 58 | counter = Counter() 59 | async for msg in self._client.iter_messages(chat, from_user=user): 60 | if not query or (msg.text and query in msg.text): 61 | if isinstance(output, Channel): 62 | url = get_url(chat, msg) 63 | await self._client.send_message(output, "{}:\n{}\n{}".format(msg.date, msg.text, url)) 64 | else: 65 | sender = user 66 | if sender is None: 67 | if msg.post: 68 | sender = chat 69 | elif msg.from_id == None: 70 | self._logger.debug(msg) 71 | continue 72 | else: 73 | sender = await self._client.get_entity(msg.from_id) 74 | self._log_message(msg, chat, sender) 75 | if print_stat: 76 | counter[msg.date.hour] += 1 77 | if print_stat: 78 | total = sum(counter.values()) 79 | for hour in range(24): 80 | print("{}: {}".format(hour, floor(counter[hour] / total * 100) * '=')) 81 | 82 | async def _get_entity(self, entity_like): 83 | try: 84 | entity = await self._client.get_entity(entity_like) 85 | except Exception as e: 86 | entity = await self._client.get_entity(int(entity_like)) 87 | return entity 88 | 89 | def _parse_msg(self, msg, key, regex): 90 | m = re.search(r'{}=({})'.format(key, regex), msg) 91 | if m is not None: 92 | return m.groups()[0] 93 | return None 94 | 95 | async def _parse_entity(self, msg: str, entity_name: str): 96 | m = self._parse_msg(msg, entity_name, r'[0-9a-zA-Z_\-]+') 97 | if m is not None: 98 | return await self._get_entity(m) 99 | return None 100 | 101 | 102 | class Commands(object): 103 | pass 104 | 105 | 106 | class PluginMount(type): 107 | def __init__(cls, name, bases, attrs): 108 | super(PluginMount, cls).__init__(name, bases, attrs) 109 | command_name = cls.command_name if hasattr(cls, 'command_name') else camel_to_snake(cls.__name__) 110 | setattr(Commands, command_name, cls) -------------------------------------------------------------------------------- /plugins/plus_mode.py: -------------------------------------------------------------------------------- 1 | import re 2 | import asyncio 3 | import traceback 4 | from datetime import timedelta 5 | 6 | from telethon import utils 7 | from telethon.sync import events 8 | from telethon.tl.functions.channels import CreateChannelRequest 9 | 10 | from plugins.base import Telegram, PluginMount 11 | 12 | 13 | class PlusMode(Telegram, metaclass=PluginMount): 14 | command_name = 'plus_mode' 15 | 16 | async def _auto_delete_async(self, msg, t, text): 17 | template = "{}\n==========\nTTL: {}" 18 | delta = timedelta(seconds=t) 19 | while delta > timedelta(0): 20 | await msg.edit(text=template.format(text, str(delta))) 21 | if delta.days > 1: 22 | await asyncio.sleep(86400) 23 | delta -= timedelta(days=1) 24 | elif delta.seconds >= 120: 25 | await asyncio.sleep(60) 26 | delta -= timedelta(seconds=60) 27 | elif delta.seconds >= 20: 28 | await asyncio.sleep(10) 29 | delta -= timedelta(seconds=10) 30 | else: 31 | await asyncio.sleep(1) 32 | delta -= timedelta(seconds=1) 33 | await msg.edit(text='[DELETED MESSAGE]') 34 | await asyncio.sleep(60) 35 | await msg.delete() 36 | 37 | def _parse_auto_delete_message(self, text): 38 | m = re.search(r'^\/([\d]+[s|m|h|d]) (.*)$', text, re.DOTALL) 39 | if m: 40 | num = int(m.group(1)[:-1]) if m.group(1)[:-1].isnumeric() else None 41 | net = m.group(1)[-1] 42 | if num is not None: 43 | if net == 'd': 44 | num *= 86400 45 | elif net == 'h': 46 | num *= 3600 47 | elif net == 'm': 48 | num *= 60 49 | return num, m.group(2) 50 | return None, None 51 | 52 | async def _auto_delete_mode(self, event, msg): 53 | if not msg.out: 54 | return 55 | channel = await event.get_chat() 56 | user = await event.get_sender() 57 | self._log_message(msg, channel, user) 58 | t, text = self._parse_auto_delete_message(msg.text) 59 | await self._auto_delete_async(msg, t, text) 60 | 61 | async def _markdown_mode(self, msg): 62 | if not msg.out: 63 | return 64 | self._logger.info(msg) 65 | try: 66 | await self._client.edit_message(msg, msg.text[4:], parse_mode='markdown') 67 | except Exception as e: 68 | self._logger.info(e) 69 | 70 | async def _getid_mode(self, msg): 71 | try: 72 | await msg.delete() 73 | me = self._client.get_me() 74 | sender = await msg.get_sender() 75 | await self._client.send_message(me, f'id: `{sender.id}`\nusername: `{sender.username}`') 76 | except Exception as _: 77 | traceback.print_exc() 78 | 79 | async def _search_mode(self, msg): 80 | try: 81 | chat = await self._parse_entity(msg.text, 'chat') 82 | user = await self._parse_entity(msg.text, 'user') 83 | query = self._parse_msg(msg.text, 'query', r'[\w]+') 84 | 85 | result = await self._client(CreateChannelRequest( 86 | "{}{}".format( 87 | '{} in '.format(utils.get_display_name(user)) if user else '', 88 | chat.title), 89 | "query={}".format(query))) 90 | 91 | created_channel = result.chats[0] 92 | self._logger.info("Channel: {} created.".format(created_channel.id)) 93 | await self._iter_messages_async(chat, user, query, created_channel) 94 | except: 95 | traceback.print_exc() 96 | finally: 97 | await msg.delete() 98 | 99 | def __call__(self): 100 | @self._client.on(events.NewMessage) 101 | async def _inner(evt): 102 | msg = evt.message 103 | if msg.text: 104 | if re.search(r'^\/([\d]+[s|m|h|d]) (.*)$', msg.text, re.DOTALL): 105 | self._logger.info("Received auto delete message: {}".format(msg.text)) 106 | await self._auto_delete_mode(evt, msg) 107 | elif msg.text.startswith('/md'): 108 | self._logger.info("Received markdown mode message: {}".format(msg.text)) 109 | await self._markdown_mode(msg) 110 | elif msg.text.startswith('search'): 111 | self._logger.info("Received search message: {}".format(msg.text)) 112 | await self._search_mode(msg) 113 | elif msg.is_reply and msg.start.startswith('/getid'): 114 | self._logger.info("Received getid message: {}".format(msg.text)) 115 | await self._getid_mode(msg) 116 | 117 | self._set_file_handler('plus_mode') 118 | self._client.start() 119 | self._client.run_until_disconnected() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

Telethon X Fire - TeleFire

3 |

4 | 5 |

A set of userful command line tools to interact with telegram.

6 | 7 |

8 | What has inside 9 | | 10 | How 11 | | 12 | Docker 13 |

14 | 15 | 16 | ## What is inside 17 | 18 | - :flags:get_all_chats: Fetches all the chat IDs and names. 19 | - :bookmark_tabs:list_messages: List messages in a certain chat. 20 | - :mag:search_messages: Search messages in a certain chat. 21 | - :skull:delete_all: Delete all the messages that you have permission to delete in a certain chat. 22 | - :question:plus_mode: Delete certain messages after certain time. 23 | - :speech_balloon:words_to_ifttt: Send an event to IFTTT when somebody said some words 24 | you interested. 25 | - :heart_eyes:special_attention_mode: Get notified when someone in special attention mode said something. 26 | 27 | ## Usage 28 | 29 | ### Setup 30 | 31 | 0. [Login to your Telegram account](https://my.telegram.org/auth) with the phone number of the account you wish to use. 32 | 1. Click **API Development tools**. 33 | 2. A `Create new application` window will appear if you didn't create one. Go head and create one. 34 | 3. Once you finish creation, get the `api_id` and `api_hash`, you will use it later. 35 | 36 | 37 | ### get_all_chats 38 | 39 | ```shell 40 | TELEGRAM_API_ID=[YOUR_API_ID] TELEGRAM_API_HASH=[YOUR_API_HASH] python telefire.py get_all_chats 41 | 42 | -100XXXXXXXXXX: CHANNEL_NAME0 43 | XXXXXXXXXX: CHANNEL_NAME1 44 | ``` 45 | Those negative IDs start with `-100` are private groups, that's the only way you can access to these groups. For public groups, you can either use id, public url, username to access to it. 46 | 47 | 48 | ### list_messages 49 | 50 | ```shell 51 | TELEGRAM_API_ID=[YOUR_API_ID] TELEGRAM_API_HASH=[YOUR_API_HASH] python telefire.py list_messages --chat [CHAT_IDENTIFIER] [Optional: --user USER_IDENTIFIER] 52 | ``` 53 | For `CHAT_IDENTIFIER`, it can be a chat ID you got from get_all_chats, or it can be something like `t.me/LGTMer` or `LGTMer`. 54 | 55 | For `USER_IDENTIFIER`, it can be the user's ID or username. 56 | 57 | 58 | ### search_messages 59 | 60 | ```shell 61 | TELEGRAM_API_ID=[YOUR_API_ID] TELEGRAM_API_HASH=[YOUR_API_HASH] python telefire.py search_messages --peer [PEER_IDENTIFIER] --query [QUERY_STRING] 62 | ``` 63 | This command comes with some optional parameters that you can custom: 64 | - `--slow`: Whether to use telegram's search API or iterate through whole message history to do the search. The later can be comprehensive if you are searching UTF-8 characters such as Chinese. 65 | - `--limit [INTEGER]`: Set the limit of search result, default `100`. 66 | - `--from_id [USER_IDENTIFIER]`: The id/username of the message sender. 67 | 68 | ### delete_all 69 | 70 | ```shell 71 | TELEGRAM_API_ID=[YOUR_API_ID] TELEGRAM_API_HASH=[YOUR_API_HASH] python telefire.py delete_all --chat [CHAT_IDENTIFIER] [Optional: --query QUERY_STRING] 72 | ``` 73 | For `CHAT_IDENTIFIER`, smiliar to `CHAT_IDENTIFIER` in get_all_chats, or it can be something like `t.me/LGTMer` or `LGTMer`. 74 | 75 | You can also using the `--query` to specify only messages containing certain string will be deleted. 76 | 77 | ### plus_mode 78 | 79 | ```shell 80 | TELEGRAM_API_ID=[YOUR_API_ID] TELEGRAM_API_HASH=[YOUR_API_HASH] python telefire.py plus_mode 81 | ``` 82 | It's a command you have to keep it running in the backgroud to use it. It's my personal favorite command! It includes several functions that's interesting and useful: 83 | - `Auto delete mode`: Add `\[NUMBER][s|m|h|d] ` before the message you want to auto delete after certain time, for example, add `\10s `(notice the space), then this message will be deleted automately after 10 seconds. you can also specify minutes(`m`), hours(`h`) and days(`d`) as the message experation time. 84 | - `Shiny mode`: just try it, add `\shiny ` to your original message!. 85 | - `Search mode`: \search [CHAT] [USERNAME] [Optional: QUERY] 86 | 87 | 88 | ### words_to_ifttt 89 | 90 | ```shell 91 | TELEGRAM_API_ID=[YOUR_API_ID] TELEGRAM_API_HASH=[YOUR_API_HASH] python telefire.py words_to_ifttt --event [IFTTT EVENT] --key [IFTTT WEBHOOK KEY] [WORDS YOU INTERESTED] 92 | ``` 93 | 94 | Like `auto_delete`, you need to keep this command running to make it work. For the `event` and `key`, you can get it from [here](https://ifttt.com/maker_webhooks). For `WORDS YOU INTERESTED`, it can be something like `telefire "telefire is so cool"`, then whenever anybody said **telefire** or **telefire is so cool**, an IFTTT event will be sent and you can create an applet to do whatever you like on IFTTT, such as sending notifications, turn on a light, etc. 95 | 96 | ### Other commands 97 | 98 | For all the others commands I didn't methtion or simply too lazy to add docs for it: 99 | ```shell 100 | python telefire.py --help 101 | ``` 102 | to get a list of all the available commands. And: 103 | ```shell 104 | python telefire.py COMMAND - --help 105 | ``` 106 | to learn how to use it. 107 | 108 | 109 | ## Docker 110 | 111 | This project also come with a `Dockerfile` so that you don't need to setup any python environment, just run the following command: 112 | ```shell 113 | docker build . -t telefire 114 | docker run -ti --rm -v $(pwd)/telefire.py:/tg/telefire.py telefire python telefire.py [COMMAND] [OPTIONS] 115 | ``` 116 | And that's it, enjoy! 117 | 118 | ## TODO 119 | 120 | - :heavy_check_mark: For deleting messages, add an option to delete messages based on time instead of always delete all. 121 | - :heavy_check_mark: A long-running service that will notify user if someone said something contains some interested words. 122 | -------------------------------------------------------------------------------- /plugins/word_cloud_inchat.py: -------------------------------------------------------------------------------- 1 | import re 2 | import math 3 | import asyncio 4 | import traceback 5 | from io import BytesIO 6 | from dateutil import parser 7 | from datetime import timezone, datetime, timedelta 8 | from collections import defaultdict 9 | 10 | from telethon import utils 11 | from telethon.sync import events 12 | # from telethon.tl.types import DocumentAttributeSticker 13 | from telethon.tl.functions.channels import CreateChannelRequest 14 | 15 | from plugins.base import Telegram, PluginMount 16 | 17 | 18 | class Action(Telegram, metaclass=PluginMount): 19 | command_name = "word_cloud_inchat" 20 | prefix = "word_cloud_inchat_" 21 | 22 | async def _generate_word_cloud_async(self, msg_id: str, reply_msg, to_chat, search_chat, user, start: datetime, end: datetime): 23 | try: 24 | import jieba 25 | from wordcloud import WordCloud 26 | except ImportError as e: 27 | print(e) 28 | return 29 | words = defaultdict(int) 30 | count = 0 31 | initial_msg = reply_msg.text + '\n' 32 | async for msg in self._client.iter_messages(search_chat, from_user=user, offset_date=end): 33 | if start and msg.date < start: 34 | break 35 | if msg.text: 36 | for word in jieba.cut(msg.text): 37 | word = word.lower() 38 | if not await self.redis.sismember(f'{self.prefix}stop_words', word): 39 | words[word] += 1 40 | # words += [w for w in jieba.cut(msg.text) if not await self.redis.sismember(f'{self.prefix}stop_words', w)] 41 | # if msg.sticker: 42 | # words += [a.alt for a in msg.sticker.attributes if isinstance(a, DocumentAttributeSticker)] 43 | 44 | count += 1 45 | if count >= 1000: 46 | p = math.floor(math.log(count, 10)) 47 | if count % int(math.pow(10, p)) == 0 and count // 1000: 48 | try: 49 | await reply_msg.edit(text=initial_msg + '.' * (count // 1000)) 50 | except Exception as _: 51 | traceback.print_exc() 52 | 53 | wordcloud_msg = None 54 | try: 55 | image = WordCloud(font_path="simsun.ttf", width=800, height=400).generate_from_frequencies(words).to_image() 56 | stream = BytesIO() 57 | image.save(stream, 'PNG') 58 | wordcloud_msg = await self._client.send_message( 59 | to_chat, 60 | '词云 for\n{}{}{}'.format( 61 | f'{search_chat.title}', 62 | f'\n{utils.get_display_name(user)}' if user else '', 63 | '\n{}-{}'.format( 64 | start.strftime('%Y/%m/%d') if start else 'Join', 65 | end.strftime('%Y/%m/%d') if end else 'Now') if start or end else ''), 66 | reply_to=msg_id, 67 | file=stream.getvalue()) 68 | except Exception as _: 69 | traceback.print_exc() 70 | finally: 71 | await reply_msg.delete() 72 | 73 | 74 | def __call__(self, redis): 75 | import aioredis 76 | @self._client.on(events.NewMessage) 77 | async def _inner(event): 78 | msg = event.message 79 | try: 80 | if msg.text and msg.text.lower().startswith('wordcloud'): 81 | to_chat = await event.get_chat() 82 | search_chat = to_chat 83 | m = re.search(r'chat=(?P[0-9a-zA-Z_\-]+)', msg.text) 84 | if m is not None: 85 | search_chat = await self._get_entity(m.groupdict().get('chat')) 86 | 87 | user = None 88 | m = re.search(r'user=(?P[0-9a-zA-Z_\-]+)', msg.text) 89 | if m is not None: 90 | user = await self._get_entity(m.groupdict().get('user')) 91 | elif msg.is_reply: 92 | reply_to = await msg.get_reply_message() 93 | user = await reply_to.get_sender() 94 | elif 'group' not in msg.text: 95 | user = await msg.get_sender() 96 | 97 | start, end = None, msg.date 98 | m = re.search(r'end=(?P[0-9_\-/: ]+)', msg.text) 99 | if m is not None: 100 | end = parser.parse(m.groupdict().get('end')) 101 | m = re.search(r'start=(?P[0-9_\-/: ]+)', msg.text) 102 | if m is not None: 103 | start = parser.parse(m.groupdict().get('start')).replace(tzinfo=timezone.utc) 104 | else: 105 | start = end - timedelta(days=1) 106 | reply_words = 'Received wordcloud request for {}{}{}'.format( 107 | f'\n{search_chat.title}', 108 | f'\n{utils.get_display_name(user)}' if user else '', 109 | '\n`{}-{}`'.format( 110 | start.strftime('%Y/%m/%d') if start else 'Join', 111 | end.strftime('%Y/%m/%d') if end else 'Now') if start or end else '') 112 | self._logger.info(reply_words.replace('\n', ' ')) 113 | reply_msg = await self._client.send_message(to_chat, reply_words, reply_to=msg.id) 114 | await self._generate_word_cloud_async(msg.id, reply_msg, to_chat, search_chat, user, start, end) 115 | except Exception as e: 116 | traceback.print_exc() 117 | 118 | async def connect_redis(): 119 | self.redis = await aioredis.create_redis_pool(f'redis://{redis}') 120 | 121 | with self._client: 122 | self._client.loop.run_until_complete(connect_redis()) 123 | 124 | self._set_file_handler("word_cloud_inchat") 125 | self._logger.info("Wordcloud inchat mode start") 126 | self._client.start() 127 | self._client.run_until_disconnected() 128 | --------------------------------------------------------------------------------