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