diff --git a/Untitled b/Untitled new file mode 100644 index 000000000..3bf9cfc1d --- /dev/null +++ b/Untitled @@ -0,0 +1,17 @@ +1.6.0: Thanks to the donations from an anonymous EFB user and zong meng. + +# Notice +* ETM: Backup your database (`plugins/eh_telegram_master/tgdata.db`) before upgrading. + +# EFB framework +* New required method for slave channels: `get_chat` + +# EFB Telegram Master +* Add detailed error dispatching mechanism +* Add mute feature +* Enhancements on multiple remote chat linking +* Multiple remote chat linking is now enabled by default. (Flag `multiple_slave_chats` now has a default value of `True`.) +* New command: `/info` — show information about the current Telegram conversation. + +# EFB WeChat Slave +* New flag: `imgcat_qr` — Render QR code in terminal in [iTerm's Image Protocol](https://www.iterm2.com/documentation-images.html). diff --git a/channel.py b/channel.py deleted file mode 100644 index 6bd59802b..000000000 --- a/channel.py +++ /dev/null @@ -1,279 +0,0 @@ -from abc import ABCMeta, abstractmethod - -# Constants Objects - -class MsgType: - Text = "Text" - Image = "Image" - Audio = "Audio" - File = "File" - Location = "Location" - Video = "Video" - Link = "Link" - Sticker = "Sticker" - Unsupported = "Unsupported" - Command = "Command" - - -class MsgSource: - User = "User" - Group = "Group" - System = "System" - - -class TargetType: - Member = "Member" - Message = "Message" - Substitution = "Substitution" - - -class ChannelType: - Master = "Master" - Slave = "Slave" - -# Objects - - -class EFBChannel: - __metaclass__ = ABCMeta - - channel_name = "Empty Channel" - channel_emoji = "?" - channel_id = "emptyChannel" - channel_type = ChannelType.Slave - queue = None - supported_message_types = set() - stop_polling = False - - def __init__(self, queue, mutex): - """ - Initialize a channel. - - Args: - queue (queue.Queue): Global message queue. - mutex (threading.Lock): Global interaction thread lock. - """ - self.queue = queue - self.mutex = mutex - - def get_extra_functions(self): - """Get a list of extra functions - - Returns: - dict: A dict of functions marked as extra functions. `methods[methodName]()` - """ - if self.channel_type == ChannelType.Master: - raise NameError("get_extra_function is not available on master channels.") - methods = {} - for mName in dir(self): - m = getattr(self, mName) - if getattr(m, "extra_fn", False): - methods[mName] = m - return methods - - @abstractmethod - def send_message(self, msg): - """ - Send message to slave channels. - - Args: - msg (EFBMsg): Message object to be sent. - - Returns: - EFBMsg: The same message object with message ID. - """ - raise NotImplementedError() - - @abstractmethod - def poll(self): - raise NotImplementedError() - - @abstractmethod - def get_chats(self): - """ - Return a list of available chats in the channel. - - Returns: - list of dict: a list of available chats in the channel. - """ - raise NotImplementedError() - - @abstractmethod - def get_chat(self, chat_uid): - """ - Return the standard chat dict of the selected chat. - Args: - chat_uid (str): UID of the chat. - - Returns: - dict: the standard chat dict of the chat. - - Raises: - KeyError: Chat is not found in the channel. - """ - raise NotImplementedError() - -class EFBMsg: - """A message. - - Attributes: - attributes (dict): Attributes used for a specific message type - channel_emoji (str): Emoji Icon for the source Channel - channel_id (str): ID for the source channel - channel_name (str): Name of the source channel - destination (dict): Destination (may be a user or a group) - member (dict): Author of this msg in a group. `None` for private messages. - origin (dict): Origin (may be a user or a group) - source (MsgSource): Source of message: User/Group/System - target (dict): Target (refers to @ messages and "reply to" messages.) - text (str): text of the message - type (MsgType): Type of message - uid (str): Unique ID of message - url (str): URL of multimedia file/Link share. `None` if N/A - path (str): Local path of multimedia file. `None` if N/A - file (file): File object to multimedia file, type "ra". `None` if N/A - mime (str): MIME type of the file. `None` if N/A - filename (str): File name of the multimedia file. `None` if N/A - - `target`: - There are 3 types of targets: `Member`, `Message`, and `Substitution` - - TargetType: Member - This is for the case where the message is targeting to a specific member in the group. - `target['target']` here is a `user dict`. - - Example: - ``` - target = { - 'type': TargetType.Member, - 'target': { - "name": "Target name", - 'alias': 'Target alias', - 'uid': 'Target UID', - } - } - ``` - - TargetType: Message - This is for the case where the message is directly replying to another message. - `target['target']` here is an `EFBMsg` object. - - Example: - ``` - target = { - 'type': TargetType.Message, - 'target': EFBMsg() - } - ``` - - TargetType: Substitution - This is for the case when user "@-ed" a list of users in the message. - `target['target']` here is a dict of correspondence between - the string used to refer to the user in the message - and a user dict. - - Example: - ``` - target = { - 'type': TargetType.Substitution, - 'target': { - '@alice': { - 'name': "Alice", - 'alias': 'Alisi', - 'uid': 123456 - }, - '@bob': { - 'name': "Bob", - 'alias': 'Baobu', - 'uid': 654321 - } - } - } - ``` - - `attributes`: - A dict of attributes can be attached for some specific message types. - Please specify `None` for values not available. - - Link: - ``` - attributes = { - "title": "Title of the article", - "description": "Description of the article", - "image": "URL to the thumbnail/featured image of the article", - "url": "URL to the article" - } - ``` - - Location: - ``` - text = "Name of the location" - attributes = { - "longitude": float("A float number indicating longitude"), - "latitude": float("A float number indicating latitude") - } - ``` - - Command: - Messages with type `Command` allow user to take action to - a specific message, including vote, add friends, etc. - - Example: - ``` - attributes = { - "commands": [ - { - "name": "A human-readable name for the command", - "callable": "name to the callable function in your channel object", - "args": [ - "a list of positional parameters passed to your function" - ], - "kwargs": { - "desc": "a dict of keyword parameters passed to your function" - } - }, - { - "name": "Greet @blueset on Telegram", - "callable": "send_message_by_username", - "args": [ - "blueset", - "Hello!" - ], - "kwargs": {} - } - ] - } - ``` - """ - channel_name = "Empty Channel" - channel_emoji = "?" - channel_id = "emptyChannel" - source = MsgSource.User - type = MsgType.Text - member = None - origin = { - "name": "Origin name", - 'alias': 'Origin alias', - 'uid': 'Origin UID', - } - destination = { - "channel": "channel_id", - "name": "Destination name", - 'alias': 'Destination alias', - 'uid': 'Destination UID', - } - target = None - uid = None - text = "" - url = None - path = None - file = None - mime = None - filename = None - attributes = {} - - def __init__(self, channel=None): - if isinstance(channel, EFBChannel): - self.channel_name = channel.channel_name - self.channel_emoji = channel.channel_emoji - self.channel_id = channel.channel_id diff --git a/docs/API/channel.rst b/docs/API/channel.rst new file mode 100644 index 000000000..feb4651ee --- /dev/null +++ b/docs/API/channel.rst @@ -0,0 +1,7 @@ +EFBChannel +========== + +This is the documentation for a EFB :term:`Channel`. + +.. autoclass:: ehforwarderbot.channel.EFBChannel + :members: diff --git a/docs/API/chat.rst b/docs/API/chat.rst new file mode 100644 index 000000000..662395bf7 --- /dev/null +++ b/docs/API/chat.rst @@ -0,0 +1,4 @@ +EFBChat +======= +.. autoclass:: ehforwarderbot.chat.EFBChat + :members: diff --git a/docs/API/constants.rst b/docs/API/constants.rst new file mode 100644 index 000000000..a1c2f5cc0 --- /dev/null +++ b/docs/API/constants.rst @@ -0,0 +1,6 @@ +Constants +========= + +.. automodule:: ehforwarderbot.constants + :members: + :undoc-members: diff --git a/docs/API/index.rst b/docs/API/index.rst new file mode 100644 index 000000000..304b306cf --- /dev/null +++ b/docs/API/index.rst @@ -0,0 +1,7 @@ +API Documentations +================== + +.. toctree:: + :glob: + + * diff --git a/docs/API/message.rst b/docs/API/message.rst new file mode 100644 index 000000000..9925e7df2 --- /dev/null +++ b/docs/API/message.rst @@ -0,0 +1,4 @@ +EFBMsg +====== +.. autoclass:: ehforwarderbot.message.EFBMsg + :members: diff --git a/docs/channel.md b/docs/channel.md index bd976f7ff..caeb1a6fa 100644 --- a/docs/channel.md +++ b/docs/channel.md @@ -76,7 +76,7 @@ Returns a `list` of `dict`s for available chats in the channel. Each `dict` shou "name": "Name of the chat", "alias": "Alternative name of the chat (alias, nickname, remark name, contact name, etc)", # None if N/A "uid": "Unique ID of the chat", - "type": MsgSource.User # or MsgSource.Group + "type": ChatType.User # or ChatType.Group } ``` diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..9985c4c84 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 + +import sphinx_rtd_theme + + +# -*- coding: utf-8 -*- +# +# EH Forwarder Bot documentation build configuration file, created by +# sphinx-quickstart on Tue Feb 28 10:17:32 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages', + 'sphinx.ext.napoleon'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'EH Forwarder Bot' +copyright = '2017, Eana Hufwe' +author = 'Eana Hufwe' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '2.0' +# The full version, including alpha/beta/rc tags. +release = '2.0.0-alpha.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# html_theme = 'alabaster' +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'ehForwarderBotDoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'ehForwarderBot.tex', 'EH Forwarder Bot Documentation', + 'Eana Hufwe', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'ehforwarderbot', 'EH Forwarder Bot Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'ehForwarderBot', 'EH Forwarder Bot Documentation', + author, 'ehForwarderBot', 'One line description of project.', + 'Miscellaneous'), +] + +# Napoleon settings +napoleon_google_docstring = True +napoleon_numpy_docstring = False +napoleon_include_init_with_doc = True +napoleon_include_private_with_doc = False +napoleon_include_special_with_doc = True +napoleon_use_admonition_for_examples = False +napoleon_use_admonition_for_notes = True +napoleon_use_admonition_for_references = False +napoleon_use_ivar = False +napoleon_use_param = True +napoleon_use_rtype = True diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..fc1c12618 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,117 @@ +EH Forwarder Bot +================ + +.. image:: https://img.shields.io/badge/Python-3.x-blue.svg + :alt: Python 3.x + :target: https://www.python.org/ +.. image:: https://img.shields.io/gitter/room/blueset/ehForwarderBot.svg + :alt: Gitter + :target: https://gitter.im/blueset/ehForwarderBot +.. image:: https://img.shields.io/badge/Chat-on%20Telegram-blue.svg + :alt: Telegram support group + :target: https://telegram.me/efbsupport +.. image:: https://readthedocs.org/projects/ehforwarderbot/badge/?version=latest + :alt: Docs\: Stable version + :target: https://ehforwarderbot.readthedocs.io/en/latest/ +.. image:: https://readthedocs.org/projects/ehforwarderbot/badge/?version=dev + :alt: Docs: Development version version + :target: https://ehforwarderbot.readthedocs.io/en/dev/ +.. image:: https://img.shields.io/github/tag/blueset/ehforwarderbot.svg + :alt: tag release + :target: https://github.com/blueset/ehForwarderBot/releases +.. image:: http://isitmaintained.com/badge/resolution/blueset/ehforwarderbot.svg + :alt: Average time to resolve an issue + :target: http://isitmaintained.com/project/blueset/ehforwarderbot +.. image:: http://isitmaintained.com/badge/open/blueset/ehforwarderbot.svg + :alt: Percentage of issues still open + :target: http://isitmaintained.com/project/blueset/ehforwarderbot +.. image:: https://img.shields.io/codacy/grade/3b2555f9134844e3b01b00700bc43eeb.svg + :alt: Codacy grade + :target: https://www.codacy.com/app/blueset/ehForwarderBot + + +.. image:: https://images.1a23.com/upload/images/SPET.png + :alt: Banner + + +*Codename* **EH Forwarder Bot** (EFB) is an extensible chat tunnel framework which allows users to contact people from other chat platforms, and ultimately remotely control their accounts in other platforms. + + +.. toctree:: + + API/index + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +Media coverage +============== + +* `Appinn: Send and Receive messages from WeChat on Telegram `_ |br| + *(EH Forwarder Bot – 在 Telegram 收发「微信」消息)* +* `Livc: Telegram — the true definition of IM `_ |br| + *(Telegram——真正定义即时通讯)* + +Glossary +======== +.. glossary:: + + Channel + A class that communicates with a chat platform, also known as a plugin. + + EFB + abbreviation for EH Forwarder Bot, this project. + + Master Channel + A channel linked to the platform which directly interact with the user. + + Plugin + See "channel". + + Slave Channel + A channel linked to the platform which is controlled by the user through EFB framework. + +Feel like contributing? +======================= + +Anyone is welcomed to raise an issue or submit a pull request, +just remember to read through and understand the `contribution guideline `_ before you do so. + +Related articles +================ + +* `Idea: Group Chat Tunneling (Sync) with EH Forwarder Bot `_ +* `EFB How-to: Send and Receive Messages from WeChat on Telegram (zh-CN) `_ (Out-dated) |br| + *(安装并使用 EFB:在 Telegram 收发微信消息,已过时)* + +License +======= + +EFB framework is licensed under `GNU General Public License 3.0 `_. + +.. code-block:: none + + EH Forwarder Bot: An extensible chat tunneling bot framework. + Copyright (C) 2016 Eana Hufwe + All rights reserved. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + +.. |br| raw:: html + +
diff --git a/docs/message.md b/docs/message.md index 796f356d0..36b1c8a40 100644 --- a/docs/message.md +++ b/docs/message.md @@ -10,10 +10,10 @@ msg = EFBMsg(self) ## Basic properties * `channel_name`, `channel_emoji`, `channel_id` should be set during the initialization process. -* `source`: the type of sender of the message, a `MsgSource` object. - Possible values: `MsgSource.User`, `MsgSource.Group`, `MsgSource.System` +* `source`: the type of sender of the message, a `ChatType` object. + Possible values: `ChatType.User`, `ChatType.Group`, `ChatType.System` * `type`: Type of message, a `MsgType` object, default to `MsgType.Text`. Will introduce it in later part. -* `member`: The member in a group who sent the message. A "User dict" or `None`. Only available when `msg.source == MsgSource.Group`. +* `member`: The member in a group who sent the message. A "User dict" or `None`. Only available when `msg.source == ChatType.Group`. * `origin`: The sender, a user or a group who sent the message. A "User dict". * `destination`: The destination user/group of the message. A "User dict", `"channel"` property is required, and shall not equal to the `channel_id` of the message. * `target`: A "Target dict" or none. Used when the message is reply to or referring to another message or user. diff --git a/ehforwarderbot/__init__.py b/ehforwarderbot/__init__.py new file mode 100644 index 000000000..94148ff03 --- /dev/null +++ b/ehforwarderbot/__init__.py @@ -0,0 +1 @@ +__version__ = "2.0.0.a1" diff --git a/ehforwarderbot/channel.py b/ehforwarderbot/channel.py new file mode 100644 index 000000000..bfe7ca00b --- /dev/null +++ b/ehforwarderbot/channel.py @@ -0,0 +1,104 @@ +from abc import ABCMeta, abstractmethod +from .constants import * + + +class EFBChannel: + """ + The abstract channel class. + + Attributes: + channel_name (str): Name of the channel. + channel_emoji (str): Emoji icon of the channel. + channel_id (str): Unique ID of the channel. + Recommended format: ``"{author}_{name}_{type}"``, + e.g. ``"eh_telegram_master"``. + channel_type (:obj:`ehforwarderbot.ChannelType`): Type of the channel. + queue (queue.Queue): Global message queue. + mutex (threading.Lock): Global interaction thread lock. + """ + __metaclass__ = ABCMeta + + channel_name = "Empty Channel" + channel_emoji = "?" + channel_id = "emptyChannel" + channel_type = ChannelType.Slave + queue = None + supported_message_types = set() + stop_polling = False + + def __init__(self, queue, mutex, slaves=None): + """ + Initialize a channel. + + Args: + queue (queue.Queue): Global message queue. + mutex (threading.Lock): Global interaction thread lock. + slaves (`obj`:dict:, optional): List of slave channels. Only + offered to master channel. + """ + self.queue = queue + self.mutex = mutex + self.slaves = slaves + + def get_extra_functions(self): + """Get a list of extra functions + + Returns: + dict[str: callable]: A dict of functions marked as extra functions. + Method can be called with ``get_extra_functions()["methodName"]()``. + """ + if self.channel_type == ChannelType.Master: + raise NameError("get_extra_function is not available on master channels.") + methods = {} + for mName in dir(self): + m = getattr(self, mName) + if getattr(m, "extra_fn", False): + methods[mName] = m + return methods + + @abstractmethod + def send_message(self, msg): + """ + Send message to slave channels. + + Args: + msg (:obj:`ehforwarderbot.message.EFBMsg`): Message object to be sent. + + Returns: + :obj:`ehforwarderbot.message.EFBMsg`: The same message object with message ID. + """ + raise NotImplementedError() + + @abstractmethod + def poll(self): + raise NotImplementedError() + + @abstractmethod + def get_chats(self): + """ + Return a list of available chats in the channel. + + Returns: + list[:obj:`ehforwarderbot.chat.EFBChat`]: a list of available chats in the channel. + """ + raise NotImplementedError() + + @abstractmethod + def get_chat(self, chat_uid, member_uid=None): + """ + Return the standard chat dict of the selected chat. + + Args: + chat_uid (str): UID of the chat. + member_uid (:obj:`str`, optional): UID of group member, + only when the selected chat is a group. + + Returns: + :obj:`ehforwarderbot.chat.EFBChat`: the standard chat dict of the chat. + + Raises: + KeyError: Chat is not found in the channel. + ValueError: ``member_uid`` is provided but chat indicated is + not a group. + """ + raise NotImplementedError() diff --git a/channelExceptions.py b/ehforwarderbot/channel_exceptions.py similarity index 100% rename from channelExceptions.py rename to ehforwarderbot/channel_exceptions.py diff --git a/ehforwarderbot/chat.py b/ehforwarderbot/chat.py new file mode 100644 index 000000000..9a7d3dc99 --- /dev/null +++ b/ehforwarderbot/chat.py @@ -0,0 +1,60 @@ +from .channel import EFBChannel +from .constants import ChatType + + +class EFBChat: + channel_id = None + channel_emoji = None + channel_name = None + chat_name = None + chat_type = None + chat_alias = None + chat_uid = None + is_chat = True + + _members = [] + + chat = None + + @property + def members(self): + return self._members.copy() + + @members.setter + def members(self, value): + self._members = value.copy() + + def __init__(self, channel=None): + if isinstance(channel, EFBChannel): + self.channel_name = channel.channel_name + self.channel_emoji = channel.channel_emoji + self.channel_id = channel.channel_id + + def self(self): + """ + Set the chat as yourself. + In this context, "yourself" means the user behind the master channel. + Every channel should relate this to the corresponding target. + + Returns: + EFBChat: This object. + """ + self.chat_name = "You" + self.chat_alias = None + self.chat_uid = "__self__" + self.chat_type = ChatType.User + return self + + def system(self): + """ + Set the chat as a system chat. + Only set for channel-level and group-level system chats. + + Returns: + EFBChat: This object. + """ + self.chat_name = "System" + self.chat_alias = None + self.chat_uid = "__system__" + self.chat_type = ChatType.User + return self diff --git a/ehforwarderbot/constants.py b/ehforwarderbot/constants.py new file mode 100644 index 000000000..babd73637 --- /dev/null +++ b/ehforwarderbot/constants.py @@ -0,0 +1,28 @@ +class MsgType: + Text = "Text" + Image = "Image" + Audio = "Audio" + File = "File" + Location = "Location" + Video = "Video" + Link = "Link" + Sticker = "Sticker" + Unsupported = "Unsupported" + Command = "Command" + + +class ChatType: + User = "User" + Group = "Group" + System = "System" + + +class TargetType: + Member = "Member" + Message = "Message" + Substitution = "Substitution" + + +class ChannelType: + Master = "Master" + Slave = "Slave" diff --git a/daemon.py b/ehforwarderbot/daemon.py similarity index 96% rename from daemon.py rename to ehforwarderbot/daemon.py index e702a46d8..42547d1fc 100644 --- a/daemon.py +++ b/ehforwarderbot/daemon.py @@ -22,6 +22,7 @@ import signal import fcntl import subprocess +import argparse try: import cPickle as pickle @@ -206,7 +207,7 @@ def list(self, name=None, group=None): if dm.chdir: lines.append('Chdir: %s' % repr(dm.chdir)) if dm.name: - lines.append('Name: %s' % dm.name) + lines.append('Profile: %s' % dm.name) if dm.group: lines.append('Group: %s' % dm.group) lines.append('Start at: "%s"' % dm.time) @@ -301,15 +302,16 @@ def transcript(path, reset=False): def main(): transcript_path = "EFB.log" - instance_name = str(crc32(os.path.dirname(os.path.abspath(inspect.stack()[0][1])).encode())) if len(sys.argv) < 2: help() exit() dm = DM() + + # instance_name = str(crc32(os.path.dirname(os.path.abspath(inspect.stack()[0][1])).encode())) + argp = argparse.ArgumentParser() + argp.add_argument("-p", "--profile") + instance_name = argp.parse_args(sys.argv[2:])[0].profile or "default" efb_args = " ".join(sys.argv[2:]) - if len(dm.get_daemons(name="EFB")): - print("Old daemon process is killed.") - dm.kill(name="EFB", quiet=True, sigkill=True) if sys.argv[1] == "start": dm.run(cmdline=" ".join((sys.executable + " main.py", efb_args)), name=instance_name, @@ -318,7 +320,7 @@ def main(): elif sys.argv[1] == "stop": dm.kill(name=instance_name, quiet=True, sigkill=True) elif sys.argv[1] == "status": - dm.list(name=instance_name) + dm.list() elif sys.argv[1] == "restart": kwargs = {"name": instance_name, "quiet": True, "sigkill": True} if len(sys.argv) > 2: diff --git a/main.py b/ehforwarderbot/efb.py similarity index 88% rename from main.py rename to ehforwarderbot/efb.py index 822c8a4cd..af860a45a 100644 --- a/main.py +++ b/ehforwarderbot/efb.py @@ -1,16 +1,17 @@ -import config +import os import queue import threading import logging import argparse import sys import signal -from channel import EFBChannel +from . import config +from .channel import EFBChannel if sys.version_info.major < 3: raise Exception("Python 3.x is required. Your version is %s." % sys.version) -__version__ = "1.6.0" +__version__ = "2.0.0.a1" parser = argparse.ArgumentParser(description="EH Forwarder Bot is an extensible chat tunnel framework which allows " "users to contact people from other chat platforms, and ultimately " @@ -23,6 +24,8 @@ version="EH Forwarder Bot %s" % __version__) parser.add_argument("-l", "--log", help="Set log file path.") +parser.add_argument("-p", "--profile", + help="Choose a profile to start with.") args = parser.parse_args() @@ -34,7 +37,7 @@ slave_threads = None -def stop_gracefully(*args, **kwargs): +def stop_gracefully(sig, stack): l = logging.getLogger("ehForwarderBot") if isinstance(master, EFBChannel): master.stop_polling = True @@ -77,14 +80,17 @@ def init(): # Initialize Plug-ins Library # (Load libraries and modules and init them with Queue `q`) l = logging.getLogger("ehForwarderBot") + + conf = config.load_config() + slaves = {} - for i in config.slave_channels: + for i in conf['slave_channels']: l.critical("\x1b[0;37;46m Initializing slave %s... \x1b[0m" % str(i)) obj = getattr(__import__(i[0], fromlist=i[1]), i[1]) slaves[obj.channel_id] = obj(q, mutex) l.critical("\x1b[0;37;42m Slave channel %s (%s) initialized. \x1b[0m" % (obj.channel_name, obj.channel_id)) - l.critical("\x1b[0;37;46m Initializing master %s... \x1b[0m" % str(config.master_channel)) - master = getattr(__import__(config.master_channel[0], fromlist=config.master_channel[1]), config.master_channel[1])( + l.critical("\x1b[0;37;46m Initializing master %s... \x1b[0m" % str(conf['master_channel'])) + master = getattr(__import__(conf['master_channel'][0], fromlist=conf['master_channel'][1]), conf['master_channel'][1])( q, mutex, slaves) l.critical("\x1b[0;37;42m Master channel %s (%s) initialized. \x1b[0m" % (master.channel_name, master.channel_id)) @@ -117,13 +123,13 @@ def poll(): else: level = logging.DEBUG logging.basicConfig(format='%(asctime)s: %(name)s [%(levelname)s]\n %(message)s', level=level) - logging.getLogger('requests').setLevel(logging.CRITICAL) - logging.getLogger('urllib3').setLevel(logging.CRITICAL) - logging.getLogger('telegram.bot').setLevel(logging.CRITICAL) signal.signal(signal.SIGINT, stop_gracefully) signal.signal(signal.SIGTERM, stop_gracefully) + if args.profile: + os.environ['EFB_PROFILE'] = str(args.profile) + if getattr(args, "log", None): LOG = args.log set_log_file(LOG) diff --git a/ehforwarderbot/message.py b/ehforwarderbot/message.py new file mode 100644 index 000000000..8b39e659c --- /dev/null +++ b/ehforwarderbot/message.py @@ -0,0 +1,164 @@ +from .constants import * +from .channel import EFBChannel + + +class EFBMsg: + """A message. + + Attributes: + attributes (dict): Attributes used for a specific message type + + A dict of attributes can be attached for some specific message types. + Please specify ``None`` for values not available. + + Link:: + + attributes = { + "title": "Title of the article", + "description": "Description of the article", + "image": "URL to the thumbnail/featured image of the article", + "url": "URL to the article" + } + + Location:: + + text = "Name of the location" + attributes = { + "longitude": float("A float number indicating longitude"), + "latitude": float("A float number indicating latitude") + } + + Command: + Messages with type ``Command`` allow user to take action to + a specific message, including vote, add friends, etc. + + Example:: + + attributes = { + "commands": [ + { + "name": "A human-readable name for the command", + "callable": "name to the callable function in your channel object", + "args": [ + "a list of positional parameters passed to your function" + ], + "kwargs": { + "desc": "a dict of keyword parameters passed to your function" + } + }, + { + "name": "Greet @blueset on Telegram", + "callable": "send_message_by_username", + "args": [ + "blueset", + "Hello!" + ], + "kwargs": {} + } + ] + } + + channel_emoji (str): Emoji Icon for the source Channel + channel_id (str): ID for the source channel + channel_name (str): Name of the source channel + destination (:obj:`ehforwarderbot.chat.EFBChat`): Destination (may be a user or a group) + member (:obj:`ehforwarderbot.chat.EFBMember`): Author of this msg in a group. ``None`` for private messages. + origin (:obj:`ehforwarderbot.chat.EFBChat`): Origin (may be a user or a group) + source (:class:`ehforwarderbot.constants.ChatType`): Source of message: User/Group/System + target (dict): Target (refers to @ messages and "reply to" messages.) + + There are 3 types of targets: ``Member``, ``Message``, and ``Substitution`` + + TargetType: Member + This is for the case where the message is targeting to a specific member in the group. + ``target['target']`` here is a `user dict`. + + Example:: + + target = { + 'type': TargetType.Member, + 'target': { + "name": "Target name", + 'alias': 'Target alias', + 'uid': 'Target UID', + } + } + + + TargetType: Message + This is for the case where the message is directly replying to another message. + ``target['target']`` here is an ``EFBMsg`` object. + + Example:: + + target = { + 'type': TargetType.Message, + 'target': EFBMsg() + } + + TargetType: Substitution + This is for the case when user "@-ed" a list of users in the message. + ``target['target']`` here is a dict of correspondence between + the string used to refer to the user in the message + and a user dict. + + Example:: + + target = { + 'type': TargetType.Substitution, + 'target': { + '@alice': { + 'name': "Alice", + 'alias': 'Arisu', + 'uid': 123456 + }, + '@bob': { + 'name': "Bob", + 'alias': 'Boobu', + 'uid': 654321 + } + } + } + + text (str): text of the message + type (:obj:`ehforwarderbot.constants.MsgType`): Type of message + uid (str): Unique ID of message + url (str): URL of multimedia file/Link share. ``None`` if N/A + path (str): Local path of multimedia file. ``None`` if N/A + file (file): File object to multimedia file, type "ra". ``None`` if N/A + mime (str): MIME type of the file. ``None`` if N/A + filename (str): File name of the multimedia file. ``None`` if N/A + + """ + channel_name = "Empty Channel" + channel_emoji = "?" + channel_id = "emptyChannel" + source = ChatType.User + type = MsgType.Text + member = None + origin = None + destination = None + target = None + uid = None + text = "" + url = None + path = None + file = None + mime = None + filename = None + attributes = {} + + def __init__(self, channel=None): + """ + Initialize an EFB Message. + + Args: + channel (:obj:`ehforwarderbot.channel.EFBChannel`, optional): + Sender channel used to initialize the message. + This will set the ``channel_name``, ``channel_emoji``, and + ``channel_id`` for the message object. + """ + if isinstance(channel, EFBChannel): + self.channel_name = channel.channel_name + self.channel_emoji = channel.channel_emoji + self.channel_id = channel.channel_id diff --git a/mimetypes b/ehforwarderbot/mimetypes similarity index 100% rename from mimetypes rename to ehforwarderbot/mimetypes diff --git a/ehforwarderbot/utils.py b/ehforwarderbot/utils.py new file mode 100644 index 000000000..8af9ae87b --- /dev/null +++ b/ehforwarderbot/utils.py @@ -0,0 +1,158 @@ +import getpass +import os +from .constants import ChatType + + +class Emoji: + GROUP_EMOJI = "👥" + USER_EMOJI = "👤" + SYSTEM_EMOJI = "💻" + UNKNOWN_EMOJI = "❓" + LINK_EMOJI = "🔗" + + @staticmethod + def get_source_emoji(t): + """ + Get the Emoji for the corresponding chat type. + + Args: + t (ChatType): The chat type. + + Returns: + str: Emoji string. + """ + if t == ChatType.User: + return Emoji.USER_EMOJI + elif t == ChatType.Group: + return Emoji.GROUP_EMOJI + elif t == ChatType.System: + return Emoji.SYSTEM_EMOJI + else: + return Emoji.UNKNOWN_EMOJI + + +def extra(name, desc): + """ + Decorator for slave channel's "extra functions" interface. + + Args: + name (str): A human readable name for the function. + desc (str): A short description and usage of it. Use + `{function_name}` in place of the function name + in the description. + + Returns: + The decorated method. + """ + + def attr_dec(f): + f.__setattr__("extra_fn", True) + f.__setattr__("name", name) + f.__setattr__("desc", desc) + return f + return attr_dec + + +def get_base_path(): + """ + Get the base data path for EFB. This is defined by the environment + variable ``EFB_DATA_PATH``. + + When ``EFB_DATA_PATH`` is defined, the path is constructed by + ``$EFB_DATA_PATH/$USERNAME``. By default, this gives + ``~/.ehforwarderbot``. + + This method creates the queried path if not existing. + + Returns: + str: The base path. + """ + base_path = os.environ.get("EFB_DATA_PATH", None) + if base_path: + base_path = os.path.join(base_path, getpass.getuser(), "") + else: + base_path = os.path.expanduser("~/.ehforwarderbot/") + os.makedirs(base_path, exist_ok=True) + return base_path + + +def get_data_path(channel): + """ + Get the path for channel data. + + This method creates the queried path if not existing. + + Args: + channel (str): Channel ID + + Returns: + str: The data path of selected channel. + """ + base_path = get_base_path() + profile = get_current_profile() + data_path = os.path.join(base_path, profile, channel, "") + os.makedirs(data_path, exist_ok=True) + return data_path + + +def get_config_path(channel=None, ext="yaml"): + """ + Get path for configuration file. Defaulted to + ``~/.ehforwarderbot/profile_name/channel_id/config.yaml``. + + This method creates the queried path if not existing. The config file will + not be created, however. + + Args: + channel (str): Channel ID. + ext (:obj:`str`, optional): Extension name of the config file. + Defaulted to ``"yaml"``. + + Returns: + str: The path to the configuration file. + """ + base_path = get_base_path() + profile = get_current_profile() + if channel: + config_path = get_data_path(channel) + else: + config_path = os.path.join(base_path, profile) + os.makedirs(config_path, exist_ok=True) + return os.path.join(config_path, "config.%s" % ext) + + +def get_cache_path(channel): + """ + Get path for the channel cache directory. Defaulted to + ``~/.ehforwarderbot/cache/profile_name/channel_id``. + + This can be defined by the environment variable ``EFB_CACHE_PATH``. + When defined, the cache path is directed to + ``$EFB_CACHE_PATH/username/profile_name/channel_id``. + + This method creates the queried path if not existing. + + Args: + channel (str): Channel ID. + + Returns: + str: Cache path. + """ + base_path = os.environ.get("EFB_CACHE_PATH", None) + profile = get_current_profile() + if base_path: + base_path = os.path.join(base_path, getpass.getuser(), profile, channel, "") + else: + base_path = os.path.expanduser(os.path.join(os.path.expanduser("~/.ehforwarderbot/cache/"), profile, channel, "")) + os.makedirs(base_path, exist_ok=True) + return base_path + + +def get_current_profile(): + """ + Get the name of current profile. + + Returns: + str: Profile name. + """ + return os.environ.get("EFB_PROFILE", "default") diff --git a/plugins/eh_telegram_master/__init__.py b/plugins/eh_telegram_master/__init__.py index c2c6703e6..fb518578d 100644 --- a/plugins/eh_telegram_master/__init__.py +++ b/plugins/eh_telegram_master/__init__.py @@ -112,6 +112,11 @@ def __init__(self, queue, mutex, slaves): """ super().__init__(queue, mutex) self.slaves = slaves + + logging.getLogger('requests').setLevel(logging.CRITICAL) + logging.getLogger('urllib3').setLevel(logging.CRITICAL) + logging.getLogger('telegram.bot').setLevel(logging.CRITICAL) + try: self.bot = telegram.ext.Updater(getattr(config, self.channel_id)['token']) except (AttributeError, KeyError): diff --git a/plugins/eh_telegram_master/db.py b/plugins/eh_telegram_master/db.py index e00a0fd4b..6f92bdb4a 100644 --- a/plugins/eh_telegram_master/db.py +++ b/plugins/eh_telegram_master/db.py @@ -266,7 +266,7 @@ def set_slave_chat_info(slave_channel_id=None, slave_chat_uid (str): Slave chat UID slave_chat_name (str): Slave chat name slave_chat_alias (str): Slave chat alias, "" (empty string) if not available - slave_chat_type (channel.MsgSource): Slave chat type + slave_chat_type (channel.ChatType): Slave chat type Returns: SlaveChatInfo: The inserted or updated row diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..b88034e41 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..fff6e38d9 --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +from distutils.core import setup +setup( + name='ehforwarderbot', + packages=['ehforwarderbot'], + version='2.0.0.a1', + description='An extensible message tunneling chat bot framework.', + author='Eana Hufwe', + author_email='ilove@1a23.com', + url='https://github.com/blueset/ehforwarderbot', + license='GPL v3', + download_url='', + keywords=['', ' '], + classifiers=[ + "Development Status :: 1 - Planning", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Communications :: Chat", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Topic :: Utilities" + ], +) diff --git a/utils.py b/utils.py deleted file mode 100644 index 16daa3f86..000000000 --- a/utils.py +++ /dev/null @@ -1,28 +0,0 @@ -class Emojis: - GROUP_EMOJI = "👥" - USER_EMOJI = "👤" - SYSTEM_EMOJI = "💻" - UNKNOWN_EMOJI = "❓" - LINK_EMOJI = "🔗" - - @staticmethod - def get_source_emoji(t): - if t == "User": - return Emojis.USER_EMOJI - elif t == "Group": - return Emojis.GROUP_EMOJI - elif t == "System": - return Emojis.SYSTEM_EMOJI - else: - return Emojis.UNKNOWN_EMOJI - - -def extra(**kw): - def attr_dec(f): - if not "name" in kw or not "desc" in kw: - raise ValueError("Key `name` and `desc` is required for extra functions.") - f.__setattr__("extra_fn", True) - for i in kw: - f.__setattr__(i, kw[i]) - return f - return attr_dec