├── .github ├── CODEOWNERS ├── archives │ ├── tgcf_0.2_wiki │ │ ├── Run-on-Google-Cloud.md │ │ ├── Run-on-PythonAnywhere.md │ │ ├── _Footer.md │ │ ├── _Sidebar.md │ │ ├── Using-bot-accounts.md │ │ ├── Flood-wait-errors-and-bans.md │ │ ├── CLI-Usage.md │ │ ├── Format-text-before-sending-to-destination.md │ │ ├── Past-vs-Live-modes-explained.md │ │ ├── Text-Replacement-feature-explained.md │ │ ├── You-can-do-OCR.md │ │ ├── Run-for-free-on-Gitpod.md │ │ ├── How-to-use--watermarking-?.md │ │ ├── How-to-use-filters-?.md │ │ ├── Plugins.md │ │ ├── Deploy-to-Digital-Ocean.md │ │ ├── Install-from-source.md │ │ ├── Run-tgcf-in-past-mode-periodically.md │ │ ├── Run-on-Android-using-Termux.md │ │ ├── How-to-write-a-plugin-for-tgcf-?.md │ │ ├── Install-and-run-using-docker.md │ │ ├── Using-with-systemctl.md │ │ ├── Login-with-a-bot-or-user-account.md │ │ ├── Environment-Variables.md │ │ ├── Deploy-to-Heroku.md │ │ ├── How-to-configure-tgcf-?.md │ │ ├── Home.md │ │ └── Run-tgcf-on-Windows.md │ └── readme_old.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── quality.yml │ ├── publish.yml │ └── codeql-analysis.yml ├── FUNDING.yml ├── CONTRIBUTING.md └── pull_request_template.md ├── .gitpod.yml ├── heroku.yml ├── tgcf ├── bot │ ├── __init__.py │ ├── utils.py │ └── live_bot.py ├── __init__.py ├── plugins │ ├── caption.py │ ├── ocr.py │ ├── replace.py │ ├── fmt.py │ ├── sender.py │ ├── mark.py │ ├── filter.py │ └── __init__.py ├── const.py ├── web_ui │ ├── run.py │ ├── pages │ │ ├── 2_⭐_Admins.py │ │ ├── 6_🔬_Advanced.py │ │ ├── 1_🔑_Telegram_Login.py │ │ ├── 5_🏃_Run.py │ │ ├── 3_🔗_Connections.py │ │ └── 4_🔌_Plugins.py │ ├── password.py │ ├── 0_👋_Hello.py │ └── utils.py ├── storage.py ├── plugin_models.py ├── utils.py ├── cli.py ├── past.py ├── live.py └── config.py ├── .pre-commit-config.yaml ├── Dockerfile ├── app.json ├── LICENSE ├── Makefile ├── pyproject.toml ├── .dockerignore ├── .gitignore └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @aahnik 2 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: Dockerfile 3 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile 4 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Run-on-Google-Cloud.md: -------------------------------------------------------------------------------- 1 | coming soon ... -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Run-on-PythonAnywhere.md: -------------------------------------------------------------------------------- 1 | coming soon... -------------------------------------------------------------------------------- /tgcf/bot/__init__.py: -------------------------------------------------------------------------------- 1 | """The subpackage for interative bot for tgcf.""" 2 | 3 | from .live_bot import get_events 4 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/_Footer.md: -------------------------------------------------------------------------------- 1 | Have a question? Ask in the [discussion forum](https://github.com/aahnik/tgcf/discussions). But make sure to **read the wiki** first. -------------------------------------------------------------------------------- /tgcf/__init__.py: -------------------------------------------------------------------------------- 1 | """Package tgcf. 2 | 3 | The ultimate tool to automate custom telegram message forwarding. 4 | https://github.com/aahnik/tgcf 5 | """ 6 | 7 | from importlib.metadata import version 8 | 9 | __version__ = version(__package__) 10 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/_Sidebar.md: -------------------------------------------------------------------------------- 1 |

2 | tgcf logo 3 |

4 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Using-bot-accounts.md: -------------------------------------------------------------------------------- 1 | 2 | ## Limitations 3 | 4 | These are the limitations for using bot accounts. 5 | 6 | - Bots can't read or send messages to other bots. 7 | - Bots can get the history of a chat. So you cant run tgcf in past mode with a bot account. 8 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Flood-wait-errors-and-bans.md: -------------------------------------------------------------------------------- 1 | Flood wait errors are imposed by telegram. Telegram has a rate limit of how many messages you can send. 2 | 3 | When a flood wait error occurs, tgcf waits for the required time and then resumes. 4 | 5 | More info to be added soon! 6 | 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: 📚 Read the docs 3 | url: https://github.com/aahnik/tgcf/wiki 4 | about: Make sure to go through the documentation first 5 | - name: 🤷 Ask a question 6 | url: https://github.com/aahnik/tgcf/discussions 7 | about: Please ask and answer questions here 8 | -------------------------------------------------------------------------------- /.github/workflows/quality.yml: -------------------------------------------------------------------------------- 1 | name: Code Quality 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-python@v2 15 | - uses: pre-commit/action@v2.0.0 16 | -------------------------------------------------------------------------------- /tgcf/plugins/caption.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from tgcf.plugins import TgcfMessage, TgcfPlugin 4 | 5 | 6 | class TgcfCaption(TgcfPlugin): 7 | id_ = "caption" 8 | 9 | def __init__(self, data) -> None: 10 | self.caption = data 11 | logging.info(self.caption) 12 | 13 | def modify(self, tm: TgcfMessage) -> TgcfMessage: 14 | tm.text = f"{self.caption.header}{tm.text}{self.caption.footer}" 15 | return tm 16 | -------------------------------------------------------------------------------- /tgcf/const.py: -------------------------------------------------------------------------------- 1 | """Declare all global constants.""" 2 | 3 | COMMANDS = { 4 | "start": "Check whether I am alive", 5 | "forward": "Set a new forward", 6 | "remove": "Remove an existing forward", 7 | "help": "Learn usage", 8 | } 9 | 10 | REGISTER_COMMANDS = True 11 | 12 | KEEP_LAST_MANY = 10000 13 | 14 | CONFIG_FILE_NAME = "tgcf.config.json" 15 | CONFIG_ENV_VAR_NAME = "TGCF_CONFIG" 16 | 17 | MONGO_DB_NAME = "tgcf-config" 18 | MONGO_COL_NAME = "tgcf-instance-0" 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pycqa/isort 3 | rev: 5.10.1 4 | hooks: 5 | - id: isort 6 | entry: isort 7 | - repo: https://github.com/psf/black 8 | rev: 22.10.0 9 | hooks: 10 | - id: black 11 | language_version: python3.10 12 | entry: black 13 | - repo: https://github.com/igorshubovych/markdownlint-cli 14 | rev: v0.32.2 15 | hooks: 16 | - id: markdownlint 17 | entry: markdownlint --ignore .github 18 | -------------------------------------------------------------------------------- /tgcf/web_ui/run.py: -------------------------------------------------------------------------------- 1 | import os 2 | from importlib import resources 3 | 4 | import tgcf.web_ui as wu 5 | from tgcf.config import CONFIG 6 | 7 | package_dir = resources.path(package=wu, resource="").__enter__() 8 | 9 | def main(): 10 | print(package_dir) 11 | path = os.path.join(package_dir, "0_👋_Hello.py") 12 | os.environ["STREAMLIT_THEME_BASE"] = CONFIG.theme 13 | os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false" 14 | os.environ["STREAMLIT_SERVER_HEADLESS"] = "true" 15 | os.system(f"streamlit run {path}") 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | ENV VENV_PATH="/venv" 3 | ENV PATH="$VENV_PATH/bin:$PATH" 4 | WORKDIR /app 5 | RUN apt-get update && \ 6 | apt-get install -y --no-install-recommends apt-utils && \ 7 | apt-get upgrade -y && \ 8 | apt-get install ffmpeg tesseract-ocr -y && \ 9 | apt-get autoclean 10 | RUN pip install --upgrade poetry 11 | RUN python -m venv /venv 12 | COPY . . 13 | RUN poetry build && \ 14 | /venv/bin/pip install --upgrade pip wheel setuptools &&\ 15 | /venv/bin/pip install dist/*.whl 16 | EXPOSE 8501 17 | CMD tgcf-web 18 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/CLI-Usage.md: -------------------------------------------------------------------------------- 1 | The application `tgcf` offers a minimal command-line interface to start it. Most of the configuration is done by using the [`tgcf.config.yml`](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) file. 2 | 3 | ```shell 4 | Arguments: 5 | MODE:[past|live] Choose the mode in which you want to run tgcf. [env var: 6 | TGCF_MODE;required] 7 | 8 | 9 | Options: 10 | -l, --loud Increase output verbosity. [env var: LOUD] 11 | -v, --version Show version and exit. 12 | --help Show this message and exit. 13 | ``` -------------------------------------------------------------------------------- /tgcf/plugins/ocr.py: -------------------------------------------------------------------------------- 1 | import pytesseract 2 | from PIL import Image 3 | 4 | from tgcf.plugins import TgcfMessage, TgcfPlugin 5 | from tgcf.utils import cleanup 6 | 7 | 8 | class TgcfOcr(TgcfPlugin): 9 | id_ = "ocr" 10 | 11 | def __init__(self, data) -> None: 12 | pass 13 | 14 | async def modify(self, tm: TgcfMessage) -> TgcfMessage: 15 | 16 | if not tm.file_type in ["photo"]: 17 | return tm 18 | 19 | file = await tm.get_file() 20 | tm.text = pytesseract.image_to_string(Image.open(file)) 21 | cleanup(file) 22 | return tm 23 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Format-text-before-sending-to-destination.md: -------------------------------------------------------------------------------- 1 | The `format` plugin allows you to force a style before sending the messages to destination chat. 2 | 3 | Make sure you have read 4 | - [How to configure tgcf](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) 5 | - [Plugins](https://github.com/aahnik/tgcf/wiki/Plugins) 6 | 7 | 8 | To use the `format` plugin, put the following in your configuration file. 9 | 10 | ```yaml 11 | plugins: 12 | # ... your other plugins here 13 | format: 14 | style: bold # choose from [ bold, italics, code, strike, plain, preserve ] 15 | # ... other plugins 16 | 17 | ``` -------------------------------------------------------------------------------- /tgcf/web_ui/pages/2_⭐_Admins.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | from tgcf.config import CONFIG, read_config, write_config 4 | from tgcf.web_ui.password import check_password 5 | from tgcf.web_ui.utils import get_list, get_string, hide_st, switch_theme 6 | 7 | CONFIG = read_config() 8 | 9 | st.set_page_config( 10 | page_title="Admins", 11 | page_icon="⭐", 12 | ) 13 | hide_st(st) 14 | switch_theme(st,CONFIG) 15 | if check_password(st): 16 | 17 | CONFIG.admins = get_list(st.text_area("Admins", value=get_string(CONFIG.admins))) 18 | st.write("Add the usernames of admins. One in each line.") 19 | 20 | if st.button("Save"): 21 | write_config(CONFIG) 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | # These are supported funding model platforms 3 | 4 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 5 | patreon: # Replace with a single Patreon username 6 | open_collective: # Replace with a single Open Collective username 7 | ko_fi: # Replace with a single Ko-fi username 8 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 9 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 10 | liberapay: # Replace with a single Liberapay username 11 | issuehunt: # Replace with a single IssueHunt username 12 | otechie: # Replace with a single Otechie username 13 | custom: ['https://aahnik.dev/support'] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🎁 Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Past-vs-Live-modes-explained.md: -------------------------------------------------------------------------------- 1 | | | past | live | 2 | | -------- | ------------------------------------------------------------ | ------------------------------------------------------- | 3 | | *what* | forwards all existing messages from source to destination | instantly forwards new message in source to destination | 4 | | *usage* | make a clone or backup free books/movies channels | live syncing of channel content | 5 | | accounts | only user account is supported for past mode ([why?](https://github.com/aahnik/tgcf/discussions/126)) | both user and bot accounts supported for live mode | 6 | 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots and gifs to help explain your problem. 21 | 22 | **System information:** 23 | In which OS are you running? GIve all details of where you have deployed the application. Make sure to give detailed version information. 24 | 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | 1. Read the README and documentation thoroughly. Also watch relavant videos if availaible. 4 | 2. Create an issue with a bug or a feature request. 5 | 3. Contact me on telegram https://telegram.me/aahnikdaw and discuss the changes you want to make. 6 | 4. Follow the code style of the project. 7 | 5. You are recommended to read these additional guidelines about Pull Requests. 8 | 9 | - [The (written) unwritten guide to pull requests](https://www.atlassian.com/blog/git/written-unwritten-guide-pull-requests) 10 | - [How to write the perfect pull request](https://github.blog/2015-01-21-how-to-write-the-perfect-pull-request/) 11 | - [Open Source Pull Request Guidelines](https://opensource.creativecommons.org/contributing-code/pr-guidelines/) 12 | -------------------------------------------------------------------------------- /tgcf/plugins/replace.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Dict 3 | 4 | from pydantic import BaseModel # pylint: disable=no-name-in-module 5 | 6 | from tgcf.plugin_models import Replace 7 | from tgcf.plugins import TgcfMessage, TgcfPlugin 8 | from tgcf.utils import replace 9 | 10 | 11 | class TgcfReplace(TgcfPlugin): 12 | id_ = "replace" 13 | 14 | def __init__(self, data): 15 | self.replace = data 16 | logging.info(self.replace) 17 | 18 | def modify(self, tm: TgcfMessage) -> TgcfMessage: 19 | msg_text: str = tm.text 20 | if not msg_text: 21 | return tm 22 | for original, new in self.replace.text.items(): 23 | msg_text = replace(original, new, msg_text, self.replace.regex) 24 | tm.text = msg_text 25 | return tm 26 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tgcf", 3 | "stack": "container", 4 | "description": "The ultimate tool to automate telegram message forwarding. To learn more visit https://github.com/aahnik/tgcf/wiki/Deploy-to-Heroku", 5 | "keywords": ["telegram", "forwarding", "automation"], 6 | "website": "https://aahnik.dev", 7 | "repository": "https://github.com/aahnik/tgcf", 8 | "logo": "https://user-images.githubusercontent.com/66209958/115183360-3fa4d500-a0f9-11eb-9c0f-c5ed03a9ae17.png", 9 | "env": { 10 | "PASSWORD": { 11 | "description": "The password to protect the web interface", 12 | "value": "", 13 | "required": true 14 | }, 15 | "MONGO_CON_STR": { 16 | "description": "The connection string to mongo db database", 17 | "value": "", 18 | "required": false 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tgcf/plugins/fmt.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from enum import Enum 3 | from typing import Any, Dict 4 | 5 | from pydantic import BaseModel # pylint: disable=no-name-in-module 6 | 7 | from tgcf.plugin_models import STYLE_CODES, Format, Style 8 | from tgcf.plugins import TgcfMessage, TgcfPlugin 9 | 10 | 11 | class TgcfFmt(TgcfPlugin): 12 | id_ = "fmt" 13 | 14 | def __init__(self, data) -> None: 15 | self.format = data 16 | logging.info(self.format) 17 | 18 | def modify(self, tm: TgcfMessage) -> TgcfMessage: 19 | if self.format.style is Style.PRESERVE: 20 | return tm 21 | msg_text: str = tm.raw_text 22 | if not msg_text: 23 | return tm 24 | style = STYLE_CODES.get(self.format.style) 25 | tm.text = f"{style}{msg_text}{style}" 26 | return tm 27 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Text-Replacement-feature-explained.md: -------------------------------------------------------------------------------- 1 | For an intro to configuration [read this](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) page. 2 | 3 | ## Simple Replacement 4 | 5 | Inside your configuration under the plugins section put this: 6 | 7 | ```yaml 8 | plugins: 9 | replace: 10 | text: 11 | "god": devil 12 | "smart": idiot 13 | "original": new 14 | ``` 15 | 16 | In the above example, "god" will be replaced by "devil" and "smart" will be replaced by "idiot" and so on. 17 | 18 | ## Using Regex 19 | 20 | If you want to use regex, you can do so, by setting `regex: true`. 21 | 22 | Example: 23 | 24 | ```yaml 25 | plugins: 26 | replace: 27 | text: 28 | "regex pattern": "new word" 29 | 30 | regex: true 31 | ``` 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Packages 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | - run: pip install --upgrade poetry 15 | - name: Publish to PyPI 16 | run: poetry publish --build 17 | env: 18 | POETRY_HTTP_BASIC_PYPI_USERNAME: "__token__" 19 | POETRY_HTTP_BASIC_PYPI_PASSWORD: ${{ secrets.PYPI_TOKEN }} 20 | - name: Login to DockerHub 21 | uses: docker/login-action@v1 22 | with: 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | - name: Build docker images and publish 26 | run: make docker-release 27 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/You-can-do-OCR.md: -------------------------------------------------------------------------------- 1 | Make sure you have read 2 | - [How to configure tgcf](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) 3 | - [Plugins](https://github.com/aahnik/tgcf/wiki/Plugins) 4 | 5 | The OCR plugin allows you to do Optical Character Recognition for images. 6 | 7 | If an image is posted in source chat, the image with text (ocr) caption will be sent to the destination chats. 8 | 9 | To activate the OCR plugin, just put the line `ocr:` under the plugins section of your configuration file. 10 | 11 | ```yaml 12 | plugins: 13 | # ... your other plugins here 14 | ocr: 15 | # ... other plugins 16 | 17 | ``` 18 | 19 | If you are running on your own computer,you must have [tesseract-ocr](https://github.com/tesseract-ocr/tesseract) installed in your system for this. 20 | 21 | If you are deploying to cloud platform, or running tgcf using the Docker method as per the instructions in the wiki, then there is nothing to worry. -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Run-for-free-on-Gitpod.md: -------------------------------------------------------------------------------- 1 | Gitpod is a cloud IDE used for rapid development. 2 | 3 | If you are planning to use `tgcf`, then one way is to install it on your computer. The other way is to run it in a cloud environment. Gitpod is the easiest way to go. 4 | 5 | 1. Click this button, to open a fresh workspace in Gitpod 6 | 7 | Run on Gitpod 9 | 10 | 11 | 2. Don't worry about the huge no. of source code files in the IDE. If you are not a developer, then leave them alone. 12 | 13 | 14 | 3. `tgcf` is ready to use. 15 | ```shell 16 | tgcf --help 17 | # prints the CLI usage 18 | ``` 19 | 20 | 4. Create two new files [`.env`](https://github.com/aahnik/tgcf/wiki/Environment-Variables) and [`tgcf.config.yml`](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) and fill them up. 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/How-to-use--watermarking-?.md: -------------------------------------------------------------------------------- 1 | Make sure you have read 2 | - [How to configure tgcf](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) 3 | - [Plugins](https://github.com/aahnik/tgcf/wiki/Plugins) 4 | 5 | The `mark` plugin allows you to apply a watermark on images/videos/gifs. 6 | 7 | If an image/video/gif is posted in the source chat, the watermarked version will be sent to the destination chat. 8 | 9 | Just put this in your configuration file. 10 | 11 | ```yaml 12 | plugins: 13 | # ... your other plugins here 14 | mark: 15 | image: /path/to/image.png # the image to apply as watermark 16 | # this can be a local path, or an URL starting with https:// 17 | ``` 18 | 19 | If you are running on your own computer,you must have [ffmpeg](https://ffmpeg.org/) installed in your system for this. 20 | 21 | If you are deploying to cloud platform, or running tgcf using the Docker method as per the instructions in the wiki, then there is nothing to worry. -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Before Creating a pull request, please read the contributing guidelines thoroughly. 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | I have fully read and understood the terms and conditions laid down in the general [Contributor License Agreement](https://aahnik.github.io/aahnik/CLA.html) 10 | 11 | - [ ] I agree to distribute my code contributions under MIT License, and will not change it in the future. 12 | - [ ] I agree that any contribution once merged, cannot be taken back by me. 13 | - [ ] I will abide by the Code of Conduct 14 | - [ ] I understand that the decision of the maintainer is final and abiding. And the maintainer reserves all rights to modify my code. I also understand that the maintainer can remove my code in future, if he thinks so. 15 | - [ ] Once my contribution is merged, my name will permanently appear in the Contribtor's List of this repository. -------------------------------------------------------------------------------- /tgcf/storage.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from pymongo.collection import Collection 4 | from telethon.tl.custom.message import Message 5 | 6 | 7 | class EventUid: 8 | """The objects of this class uniquely identifies a message with its chat id and message id.""" 9 | 10 | def __init__(self, event) -> None: 11 | self.chat_id = event.chat_id 12 | try: 13 | self.msg_id = event.id 14 | except: # pylint: disable=bare-except 15 | self.msg_id = event.deleted_id 16 | 17 | def __str__(self) -> str: 18 | return f"chat={self.chat_id} msg={self.msg_id}" 19 | 20 | def __eq__(self, other) -> bool: 21 | return self.chat_id == other.chat_id and self.msg_id == other.msg_id 22 | 23 | def __hash__(self) -> int: 24 | return hash(self.__str__()) 25 | 26 | 27 | class DummyEvent: 28 | def __init__(self, chat_id, msg_id): 29 | self.chat_id = chat_id 30 | self.id = msg_id 31 | 32 | 33 | stored: Dict[EventUid, Dict[int, Message]] = {} 34 | CONFIG_TYPE: int = 0 35 | mycol: Collection = None 36 | -------------------------------------------------------------------------------- /tgcf/plugins/sender.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | from tgcf.plugins import TgcfMessage, TgcfPlugin 5 | from tgcf.config import CONFIG, get_SESSION 6 | from telethon import TelegramClient 7 | 8 | class TgcfSender(TgcfPlugin): 9 | id_ = "sender" 10 | 11 | async def __ainit__(self) -> None: 12 | sender = TelegramClient( 13 | get_SESSION(CONFIG.plugins.sender, 'tgcf_sender'), 14 | CONFIG.login.API_ID, 15 | CONFIG.login.API_HASH, 16 | ) 17 | if self.data.user_type == 0: 18 | if self.data.BOT_TOKEN == "": 19 | logging.warning("[Sender] Bot token not found, but login type is set to bot.") 20 | sys.exit() 21 | await sender.start(bot_token=self.data.BOT_TOKEN) 22 | else: 23 | await sender.start() 24 | self.sender = sender 25 | 26 | async def modify(self, tm: TgcfMessage) -> TgcfMessage: 27 | tm.client = self.sender 28 | if tm.file_type != "nofile": 29 | tm.new_file = await tm.get_file() 30 | tm.cleanup = True 31 | return tm -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aahnik Daw 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 | -------------------------------------------------------------------------------- /tgcf/web_ui/password.py: -------------------------------------------------------------------------------- 1 | from tgcf.config import PASSWORD 2 | 3 | 4 | def check_password(st): 5 | """Returns `True` if the user had the correct password.""" 6 | 7 | def password_entered(): 8 | """Checks whether a password entered by the user is correct.""" 9 | if st.session_state["password"] == PASSWORD: 10 | st.session_state["password_correct"] = True 11 | del st.session_state["password"] # don't store password 12 | else: 13 | st.session_state["password_correct"] = False 14 | 15 | if "password_correct" not in st.session_state: 16 | # First run, show input for password. 17 | st.text_input( 18 | "Password", type="password", on_change=password_entered, key="password" 19 | ) 20 | return False 21 | elif not st.session_state["password_correct"]: 22 | # Password not correct, show input + error. 23 | st.text_input( 24 | "Password", type="password", on_change=password_entered, key="password" 25 | ) 26 | st.error("😕 Password incorrect") 27 | return False 28 | else: 29 | # Password correct. 30 | return True 31 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/How-to-use-filters-?.md: -------------------------------------------------------------------------------- 1 | Filters allow you to selectively forward some messages while excluding others. 2 | 3 | 4 | 5 | For an intro to configuration [read this](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) page. 6 | 7 | ## Example 8 | 9 | ```yaml 10 | plugins: 11 | filter: 12 | 13 | text: 14 | case_sensitive: true # default is false if you don't write this line 15 | # the case_sensitive param has no significance if regex is set to true 16 | whitelist: ["this word"] 17 | blacklist: ["hello"] 18 | regex: false # default is false. 19 | # set to true if you want the expressions in whitelist and blacklist 20 | # to be evaluated as regular expressions 21 | 22 | users: 23 | blacklist: [1547315064] # currently user ids are supported only. 24 | # get user ids from @userinfobot on telegram 25 | 26 | files: 27 | whitelist: [document,nofile] 28 | # valid types are 29 | # audio,gif,video,video_note,sticker,contact,photo,document,nofile 30 | 31 | 32 | ``` 33 | 34 | Note: 35 | - for text filtering, you may use whitelist or blacklist or both 36 | - for users and files filtering, use either a whitelist or a blacklist 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # lists all available targets 2 | list: 3 | @sh -c "$(MAKE) -p no_targets__ | \ 4 | awk -F':' '/^[a-zA-Z0-9][^\$$#\/\\t=]*:([^=]|$$)/ {\ 5 | split(\$$1,A,/ /);for(i in A)print A[i]\ 6 | }' | grep -v '__\$$' | grep -v 'make\[1\]' | grep -v 'Makefile' | sort" 7 | 8 | # required for list 9 | no_targets__: 10 | 11 | VERSION=$$(poetry version -s) 12 | 13 | clean: 14 | @rm -rf build dist .eggs *.egg-info 15 | @rm -rf .benchmarks .coverage coverage.xml htmlcov report.xml .tox 16 | @find . -type d -name '.mypy_cache' -exec rm -rf {} + 17 | @find . -type d -name '__pycache__' -exec rm -rf {} + 18 | @find . -type d -name '*pytest_cache*' -exec rm -rf {} + 19 | @find . -type f -name "*.py[co]" -exec rm -rf {} + 20 | 21 | fmt: clean 22 | @poetry run isort . 23 | @poetry run black . 24 | 25 | hard-clean: clean 26 | @rm -rf .venv 27 | 28 | ver: 29 | @echo tgcf $(VERSION) 30 | 31 | pypi: 32 | @poetry publish --build 33 | 34 | docker: 35 | @docker build -t tgcf . 36 | @docker tag tgcf aahnik/tgcf:latest 37 | @docker tag tgcf aahnik/tgcf:$(VERSION) 38 | 39 | docker-release: docker 40 | @docker push -a aahnik/tgcf 41 | 42 | docker-run: 43 | @docker run -d -p 8501:8501 --env-file .env aahnik/tgcf 44 | 45 | release: pypi docker-release 46 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Plugins.md: -------------------------------------------------------------------------------- 1 | 2 | ## Official Plugins 3 | 4 | The official plugins are installed by default when you install `tgcf`. The source code of these plugins lies under the [`plugins` ](https://github.com/aahnik/tgcf/tree/main/tgcf/plugins) subpackage of `tgcf`. 5 | 6 | Some of the plugins may have additional non-python dependencies, which you need to manually install. Click on the link to the plugin to learn how to use it. 7 | 8 | 1. [Filter](https://github.com/aahnik/tgcf/wiki/How-to-use-filters-%3F) 9 | 2. [Replace](https://github.com/aahnik/tgcf/wiki/Text-Replacement-feature-explained) 10 | 3. [Format](https://github.com/aahnik/tgcf/wiki/Format-text-before-sending-to-destination) 11 | 4. [Watermark](https://github.com/aahnik/tgcf/wiki/How-to-use--watermarking-%3F) 12 | 5. [OCR](https://github.com/aahnik/tgcf/wiki/You-can-do-OCR) 13 | 14 | More plugins coming soon! 15 | 16 | ## Third-Party Plugins 17 | 18 | Third-party plugins are those which are developed by other developers. They live in their own repositories. If you have built a `tgcf` plugin, then you can list them here. Just send me a message on [Telegram](https://telegram.me/aahnikdaw). 19 | 20 | Read the tutorial [How to write a plugin for `tgcf`](https://github.com/aahnik/tgcf/wiki/How-to-write-a-plugin-for-tgcf-%3F) for more information. 21 | 22 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Deploy-to-Digital-Ocean.md: -------------------------------------------------------------------------------- 1 | DigitalOcean infrastructure is a leading cloud service provider based in the United States of America. Their headquarter operates from New York City, and their data centers are prevalent in every corner of the world in order to provide seamless cloud services across the globe. 2 | 3 | 4 | ## App Platform 5 | 6 | 7 | ![create-app](https://user-images.githubusercontent.com/66209958/113475188-aab3a200-9491-11eb-8649-9c4111d05a1b.png) 8 | 9 | Click **Create** -> *Apps* 10 | 11 | 12 | ![source-is-docker-hub](https://user-images.githubusercontent.com/66209958/113475207-c1f28f80-9491-11eb-84d1-5b90e6a4ee3c.png) 13 | 14 | Choose **Docker Hub** as the source. 15 | Choose the **type** as _"Worker"_ (as we are not making any web app). 16 | 17 | In the next step,the **repository** path is _"aahnik/tgcf"_. 18 | 19 | ![type-worker](https://user-images.githubusercontent.com/66209958/113475243-fbc39600-9491-11eb-9c2e-96fb0487d43d.png) 20 | 21 | You can now set the values of the [environment variables](https://github.com/aahnik/tgcf/wiki/Environment-Variables) from this beautiful interface provided by Digital Ocean. 22 | 23 | 24 | Give any name to your app. After this, you will be lead to a pricing page. Choose a pricing plan suitable for you and click "Launch basic app". 25 | 26 | 27 | 28 | ## Ubuntu Droplet 29 | 30 | If you want more control, you may run `tgcf` on a VPS like DigitalOcean's ubuntu droplets. 31 | 32 | Steps: 33 | - Create a Droplet 34 | - SSH into it 35 | - Update packages 36 | - Install python 37 | - Install `tgcf` 38 | - Use `tgcf` CLI 39 | 40 | Details coming soon! -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Install-from-source.md: -------------------------------------------------------------------------------- 1 | Warning: This method of installation of `tgcf` is only for python developers, and not recommended for normal users. 2 | 3 | ## Requirements 4 | 5 | | Thing | Why | 6 | | -------------------------------------------- | ------------------------------------------------------ | 7 | | [`git `](https://git-scm.com/) | to clone the repo and for version control | 8 | | [`python`](https://www.python.org/) | language tgcf is written | 9 | | [`poetry`](https://python-poetry.org/) | used for package management | 10 | | [`docker`](https://www.docker.com/) | if you want to build docker images or run using docker | 11 | | [`make`](https://www.gnu.org/software/make/) | if you are interested in developing | 12 | 13 | 14 | 15 | 16 | ## Steps 17 | 18 | 1. Clone the repo and move into it 19 | ```shell 20 | git clone https://github.com/aahnik/tgcf.git && cd tgcf 21 | ``` 22 | 23 | 2. Install dependencies with `poetry` 24 | ```shell 25 | poetry install 26 | ``` 27 | > Don't have poetry? Run `pip install pipx` and then `pipx install poetry`. To add poetry to path, run `pipx ensurepath` 28 | 29 | 3. Activate the virtual environment 30 | ```shell 31 | 32 | poetry shell 33 | ``` 34 | 35 | 4. Now the `tgcf` command is available to you. 36 | ```shell 37 | tgcf --help 38 | ``` 39 | 40 | 5. To fetch updates from GitHub 41 | ```shell 42 | git fetch && git pull 43 | ``` 44 | Now, go back to step 2 to install the updates. 45 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Run-tgcf-in-past-mode-periodically.md: -------------------------------------------------------------------------------- 1 | You can set up cron jobs! 2 | 3 | for free using GitHub Actions scheduled workflows. 4 | 5 | Or, you may even trigger the workflow manually! 6 | 7 | How to do ? 8 | 9 | Step1. Go to https://github.com/aahnik/tgcf-on-gh-action 10 | 11 | Step2. Click on the "use this template" button 12 | 13 | ![image](https://user-images.githubusercontent.com/66209958/117601342-b880d500-b16b-11eb-91a2-2f1bf1ccec6a.png) 14 | 15 | Step3. Give any interesting name to your repo, and make it public (unlimited free) or private (limited to action minutes per month). 16 | 17 | ![image](https://user-images.githubusercontent.com/66209958/117601415-f251db80-b16b-11eb-85ca-24ebc28ec3f5.png) 18 | 19 | If your repo is public, only your configuration file will be visible to others. Your secrets such as API_ID, API_HASH,SESSION_STRING, are stored safe in github secrets. 20 | 21 | Step4. Go to settings -> secrets of the repo 22 | 23 | Create the following secrets 24 | 25 | 1. API_ID 26 | 2. API_HASH 27 | 3. SESSION_STRING (get it from [here](https://github.com/aahnik/tgcf/wiki/Login-with-a-bot-or-user-account#generate-session-string) ) 28 | 29 | ![image](https://user-images.githubusercontent.com/66209958/117601591-62f8f800-b16c-11eb-8b9f-a45d69afca2c.png) 30 | 31 | Step5. Edit the `tgcf.config.yml` file according to your needs, 32 | 33 | 34 | Step6. Go to the Actions tab -> Select tgcf-past -> Run workflow 35 | 36 | ![image](https://user-images.githubusercontent.com/66209958/117601708-a0f61c00-b16c-11eb-9f2b-c525b24a4064.png) 37 | 38 | To run periodically, set a schedule for `on` param in your workflow file 39 | 40 | for cron syntax use https://crontab.guru/ or https://cron.help/ 41 | 42 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "tgcf" 3 | version = "1.1.8" 4 | description = "The ultimate tool to automate custom telegram message forwarding." 5 | authors = ["aahnik "] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/aahnik/tgcf" 9 | documentation = "https://github.com/aahnik/tgcf/wiki" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.10" 13 | requests = "^2.28.1" 14 | typer = "^0.7.0" 15 | python-dotenv = "^0.21.0" 16 | pydantic = "^1.10.2" 17 | Telethon = "1.26.0" 18 | cryptg = "^0.4.0" 19 | Pillow = ">=9.3,<11.0" 20 | hachoir = "^3.1.3" 21 | aiohttp = "^3.8.3" 22 | tg-login = "^0.0.4" 23 | "watermark.py" = "^0.0.3" 24 | pytesseract = "^0.3.7" 25 | rich = "^12.6.0" 26 | verlat = "^0.1.0" 27 | streamlit = "^1.15.2" 28 | PyYAML = "^6.0" 29 | pymongo = "^4.3.3" 30 | 31 | [tool.poetry.dev-dependencies] 32 | black = {version = "^22.10.0", allow-prereleases = true} 33 | isort = "^5.10.1" 34 | pre-commit = "^2.20.0" 35 | 36 | 37 | [tool.poetry.scripts] 38 | tgcf = 'tgcf.cli:app' 39 | tgcf-web = 'tgcf.web_ui.run:main' 40 | 41 | [tool.poetry.group.dev.dependencies] 42 | ipykernel = "^6.17.0" 43 | pylint = "^2.15.8" 44 | 45 | [build-system] 46 | requires = ["poetry-core>=1.0.0"] 47 | build-backend = "poetry.core.masonry.api" 48 | 49 | 50 | [tool.isort] 51 | profile = "black" 52 | 53 | [tool.black] 54 | line-length = 88 55 | include = '\.pyi?$' 56 | exclude = ''' 57 | ( 58 | /( 59 | \.eggs # exclude a few common directories in the 60 | | \.git # root of the project 61 | | \.hg 62 | | \.mypy_cache 63 | | \.tox 64 | | \.venv 65 | | _build 66 | | buck-out 67 | | build 68 | | dist 69 | )/ 70 | | foo.py # also separately exclude a file named foo.py in 71 | # the root of the project 72 | ) 73 | ''' 74 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | tgcf.config.yml 3 | tgcf.config.json 4 | .venv 5 | .vscode 6 | .github 7 | t.py 8 | *.session 9 | *.session-journal 10 | foo.py 11 | bar.py 12 | foo.bar 13 | t.ipynb 14 | tgcf_external.py 15 | t.yml 16 | todo 17 | *.txt 18 | *.ipynb 19 | 20 | # Git 21 | .git 22 | .gitignore 23 | 24 | # CI 25 | .codeclimate.yml 26 | .travis.yml 27 | .taskcluster.yml 28 | 29 | # Docker 30 | docker-compose.yml 31 | .docker 32 | 33 | # Byte-compiled / optimized / DLL files 34 | __pycache__/ 35 | */__pycache__/ 36 | */*/__pycache__/ 37 | */*/*/__pycache__/ 38 | *.py[cod] 39 | */*.py[cod] 40 | */*/*.py[cod] 41 | */*/*/*.py[cod] 42 | 43 | # C extensions 44 | *.so 45 | 46 | # Distribution / packaging 47 | .Python 48 | env/ 49 | build/ 50 | develop-eggs/ 51 | dist/ 52 | downloads/ 53 | eggs/ 54 | lib/ 55 | lib64/ 56 | parts/ 57 | sdist/ 58 | var/ 59 | *.egg-info/ 60 | .installed.cfg 61 | *.egg 62 | 63 | # PyInstaller 64 | # Usually these files are written by a python script from a template 65 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 66 | *.manifest 67 | *.spec 68 | 69 | # Installer logs 70 | pip-log.txt 71 | pip-delete-this-directory.txt 72 | 73 | # Unit test / coverage reports 74 | htmlcov/ 75 | .tox/ 76 | .coverage 77 | .cache 78 | nosetests.xml 79 | coverage.xml 80 | 81 | # Translations 82 | *.mo 83 | *.pot 84 | 85 | # Django stuff: 86 | *.log 87 | 88 | # Sphinx documentation 89 | docs/_build/ 90 | 91 | # PyBuilder 92 | target/ 93 | 94 | # Virtual environment 95 | .env/ 96 | .venv/ 97 | venv/ 98 | 99 | # PyCharm 100 | .idea 101 | 102 | # Python mode for VIM 103 | .ropeproject 104 | */.ropeproject 105 | */*/.ropeproject 106 | */*/*/.ropeproject 107 | 108 | # Vim swap files 109 | *.swp 110 | */*.swp 111 | */*/*.swp 112 | */*/*/*.swp -------------------------------------------------------------------------------- /tgcf/web_ui/0_👋_Hello.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | from tgcf.web_ui.utils import hide_st, switch_theme 4 | from tgcf.config import read_config 5 | 6 | CONFIG = read_config() 7 | 8 | st.set_page_config( 9 | page_title="Hello", 10 | page_icon="👋", 11 | ) 12 | hide_st(st) 13 | switch_theme(st,CONFIG) 14 | st.write("# Welcome to tgcf 👋") 15 | 16 | html = """ 17 |

18 | tgcf logo 19 |

20 | """ 21 | 22 | st.components.v1.html(html, width=None, height=None, scrolling=False) 23 | with st.expander("Features"): 24 | st.markdown( 25 | """ 26 | tgcf is the ultimate tool to automate custom telegram message forwarding. 27 | 28 | The key features are: 29 | 30 | - Forward messages as "forwarded" or send a copy of the messages from source to destination chats. A chat can be anything: a group, channel, person or even another bot. 31 | 32 | - Supports two modes of operation past or live. The past mode deals with all existing messages, while the live mode is for upcoming ones. 33 | 34 | - You may login with a bot or an user account. Telegram imposes certain limitations on bot accounts. You may use an user account to perform the forwards if you wish. 35 | 36 | - Perform custom manipulation on messages. You can filter, format, replace, watermark, ocr and do whatever else you need ! 37 | 38 | - Detailed wiki + Video tutorial. You can also get help from the community. 39 | 40 | - If you are a python developer, writing plugins for tgcf is like stealing candy from a baby. Plugins modify the message before they are sent to the destination chat. 41 | 42 | What are you waiting for? Star the repo and click Watch to recieve updates. 43 | 44 | """ 45 | ) 46 | 47 | st.warning("Please press Save after changing any config.") 48 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Run-on-Android-using-Termux.md: -------------------------------------------------------------------------------- 1 | Hopefully, you have already read the README for a basic introduction to `tgcf`. 2 | 3 | The Termux app in Android offers you a full-blown Linux terminal. 4 | 5 | [![image](https://user-images.githubusercontent.com/66209958/115503616-559acd00-a294-11eb-8909-a27ff9a6efd6.png)](https://play.google.com/store/apps/details?id=com.termux&hl=en&gl=US) 6 | 7 | Install [Termux](https://play.google.com/store/apps/details?id=com.termux&hl=en&gl=US) from Google Play Store. 8 | 9 | > **Note:** Termux does not work well with Android 5 or 6. Don't worry! Most probably you have a much newer version of Android. 10 | 11 | ## Install `tgcf` on termux 12 | 13 | Just open your termux and run this: 14 | 15 | ```shell 16 | curl -Lks bit.ly/tgcf-termux | bash 17 | ``` 18 | 19 |
20 | What happens when you run the above line? 21 |
22 | 23 | - The above line (the installation command) actually fetches the installer script and runs it using bash. 24 | - Read the installer script by visiting the link [bit.ly/tgcf-termux](http://bit.ly/tgcf-termux). You may execute the lines one by one, manually. 25 | 26 |
27 | 28 | 29 | 30 | ## Testing 31 | 32 | To test if `tgcf` was properly installed, 33 | 34 | ```shell 35 | tgcf --version 36 | ``` 37 | 38 | It should output version no. and that should match with the version of the [latest release](https://github.com/aahnik/tgcf/releases). 39 | 40 | ## Configure and run 41 | 42 | Learn about 43 | - [environment variables](https://github.com/aahnik/tgcf/wiki/Environment-Variables), 44 | - [CLI usage](https://github.com/aahnik/tgcf/wiki/CLI-Usage) and 45 | - how to [configure tgcf](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F), 46 | and then start using `tgcf`. 47 | 48 | When you install `tgcf` using the above method, the text editor `micro` is also installed. You can use `micro` to edit text files. 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /tgcf/plugins/mark.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import shutil 4 | from typing import Any, Dict 5 | 6 | import requests 7 | from pydantic import BaseModel # pylint: disable=no-name-in-module 8 | from watermark import File, Position, Watermark, apply_watermark 9 | 10 | from tgcf.plugin_models import MarkConfig 11 | from tgcf.plugins import TgcfMessage, TgcfPlugin 12 | from tgcf.utils import cleanup 13 | 14 | 15 | def download_image(url: str, filename: str = "image.png") -> bool: 16 | if filename in os.listdir(): 17 | logging.info("Image for watermarking already exists.") 18 | return True 19 | try: 20 | logging.info(f"Downloading image {url}") 21 | response = requests.get(url, stream=True) 22 | if response.status_code == 200: 23 | logging.info("Got Response 200") 24 | with open(filename, "wb") as file: 25 | response.raw.decode_content = True 26 | shutil.copyfileobj(response.raw, file) 27 | except Exception as err: 28 | logging.error(err) 29 | return False 30 | else: 31 | logging.info("File created image") 32 | return True 33 | 34 | 35 | class TgcfMark(TgcfPlugin): 36 | id_ = "mark" 37 | 38 | def __init__(self, data) -> None: 39 | self.data = data 40 | 41 | async def modify(self, tm: TgcfMessage) -> TgcfMessage: 42 | if not tm.file_type in ["gif", "video", "photo"]: 43 | return tm 44 | downloaded_file = await tm.get_file() 45 | base = File(downloaded_file) 46 | if self.data.image.startswith("https://"): 47 | download_image(self.data.image) 48 | overlay = File("image.png") 49 | else: 50 | overlay = File(self.data.image) 51 | wtm = Watermark(overlay, self.data.position) 52 | tm.new_file = apply_watermark(base, wtm, frame_rate=self.data.frame_rate) 53 | cleanup(downloaded_file) 54 | tm.cleanup = True 55 | return tm 56 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/How-to-write-a-plugin-for-tgcf-?.md: -------------------------------------------------------------------------------- 1 | ### [WARNING] ❗this document is not up-to-date with the latest changes of tgcf. Please re-visit after a few days. 2 | 3 |
4 |
5 |
6 |
7 | 8 | 9 | This tutorial is for python developers. If you are a general user, you may make a feature request for a plugin you need. 10 | 11 | ## Prerequisites 12 | - Intermediate level knowledge of [Python](https://python.org) programming language 13 | - Basic knowledge of [Telethon](https://github.com/LonamiWebs/Telethon) 14 | - Some idea about how `tgcf` works 15 | 16 | 17 | Writing a plugin is a piece of cake. A plugin is basically a python module that can be imported by `tgcf`. You can even package it and publish it to PyPI for providing an easy `pip install` for your users. 18 | 19 | ## Naming Rules 20 | 21 | The plugin name (also known as plugin id) should be a single word in lowercase describing the feature. 22 | 23 | For example: if your plugin name is `hello`, then the name of the package should be `tgcf_hello`, and the name of the plugin class should be `TgcfHello`. 24 | 25 | ## Write your first plugin 26 | 27 | First of all, create a folder named `tgcf_hello`, and inside it create `__init__.py`. For the sake of simplicity, in this example, we will be writing our logic inside `__init__.py`. For complex plugins, you can have multiple modules and even sub-packages. 28 | 29 | 30 | ```python 31 | # __init__.py 32 | 33 | 34 | class TgcfHello: 35 | id = "hello" 36 | # the plugin class must have this `id` attribute 37 | 38 | def __init__(self, data): 39 | # the plugin class must have a constructor and should validate data here 40 | self.data = data 41 | 42 | def modify(self, message): 43 | # the modify method, receives the message collected by tgcf 44 | # the output of this method will be forwarded 45 | 46 | # manipulate the message here 47 | return message 48 | 49 | ``` 50 | 51 | More details to be added soon! 52 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Install-and-run-using-docker.md: -------------------------------------------------------------------------------- 1 | It is assumed that you are familiar with basic `docker` commands. Docker should be properly installed and running in your system. 2 | 3 | 4 | - Make sure you have understood how `tgcf` is run by passing certain variables via [command-line options](https://github.com/aahnik/tgcf/wiki/CLI-usage) or by setting them as [environment variables](https://github.com/aahnik/tgcf/wiki/Environment-Variables). 5 | - Read about [`tgcf.config.yml`](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) to know how you can configure `tgcf` 6 | 7 | ## Install 8 | 9 | 10 | Pull the [official docker image](https://hub.docker.com/r/aahnik/tgcf) from DockerHub. 11 | 12 | ```shell 13 | docker pull aahnik/tgcf 14 | ``` 15 | 16 | > **Tip**: Use `aahnik/tgcf:minimal` for a smaller image size. (beta) 17 | 18 | ## Configure 19 | 20 | - Write all your [environment variables](https://github.com/aahnik/tgcf/wiki/Environment-Variables#create-a-env-file) in a file called `.env`. 21 | - Write your [configuration](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) 22 | in `tgcf.config.yml`. 23 | 24 | ## Run 25 | 26 | ```shell 27 | docker run -v absolute/path/to/tgcf.config.yml:/app/tgcf.config.yml -d --env-file .env aahnik/tgcf 28 | ``` 29 | 30 | Note: 31 | - the `-d` flag tells the docker command to run the container in detached mode. 32 | - the `--env-file` option passes the file `.env` for its variables to be used inside the container. 33 | 34 | 35 | ## Check 36 | 37 | To see if your container is running, 38 | 39 | ```shell 40 | $ docker ps 41 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 42 | ae4d7d6651ca aahnik/tgcf "tgcf --loud" 3 minutes ago Up 3 minutes zen_gates 43 | 44 | ``` 45 | 46 | The container id and name will be different in your machine. 47 | 48 | To see the logs produced by the container, 49 | 50 | ```shell 51 | $ docker logs zen_gates 52 | ``` 53 | 54 | Replace `zen_gates` with the name of the container in your machine. 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /tgcf/bot/utils.py: -------------------------------------------------------------------------------- 1 | """helper functions for the bot.""" 2 | import logging 3 | from typing import List 4 | 5 | from telethon import events 6 | 7 | from tgcf import config 8 | from tgcf.config import Forward 9 | 10 | 11 | def admin_protect(org_func): 12 | """Decorate to restrict non admins from accessing the bot.""" 13 | 14 | async def wrapper_func(event): 15 | """Wrap the original function.""" 16 | logging.info(f"Applying admin protection! Admins are {config.ADMINS}") 17 | if event.sender_id not in config.ADMINS: 18 | await event.respond("You are not authorized.") 19 | raise events.StopPropagation 20 | return await org_func(event) 21 | 22 | return wrapper_func 23 | 24 | 25 | def get_args(text: str) -> str: 26 | """Return the part of message following the command.""" 27 | splitted = text.split(" ", 1) 28 | 29 | if not len(splitted) == 2: 30 | splitted = text.split("\n", 1) 31 | if not len(splitted) == 2: 32 | return "" 33 | 34 | prefix, args = splitted 35 | args = args.strip() 36 | logging.info(f"Got command {prefix} with args {args}") 37 | return args 38 | 39 | 40 | def display_forwards(forwards: List[Forward]) -> str: 41 | """Return a string that beautifully displays all current forwards.""" 42 | if len(forwards) == 0: 43 | return "Currently no forwards are set" 44 | forward_str = "This is your configuration" 45 | for forward in forwards: 46 | forward_str = ( 47 | forward_str 48 | + f"\n\n```\nsource: {forward.source}\ndest: {forward.dest}\n```\n" 49 | ) 50 | 51 | return forward_str 52 | 53 | 54 | def remove_source(source, forwards: List[Forward]) -> List[Forward]: 55 | """Remove a source from forwards.""" 56 | for i, forward in enumerate(forwards): 57 | if forward.source == source: 58 | del forwards[i] 59 | return forwards 60 | raise ValueError("The source does not exist") 61 | 62 | 63 | def get_command_prefix(): 64 | if config.is_bot is None: 65 | raise ValueError("config.is_bot is not set!") 66 | return "/" if config.is_bot else "\." 67 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Using-with-systemctl.md: -------------------------------------------------------------------------------- 1 | For more info about running tgcf in the background [read this discussion ➥](https://github.com/aahnik/tgcf/discussions/219) 2 | 3 | ## Create a service 4 | 5 | Step by step guide 6 | 7 | **Step 1.** 8 | Create an **executable shell script** to start tgcf from the proper folder. Let's name it `tgcf_start.sh` and put it in your home folder. 9 | 10 | ```shell 11 | #!/usr/bin/bash 12 | 13 | cd /home/aahnik/Desktop/tgcf # the folder in which tgcf is cloned 14 | # dont use ~ in the path, use full expanded absolute path 15 | # the folder must contain the proper .env and tgcf.config.yml files 16 | 17 | # tgcf must be installed inside a virtual env (recommended) 18 | # install tgcf using pip or clone the repo and run poetry install 19 | 20 | .venv/bin/tgcf live --loud 21 | ``` 22 | 23 | Make the script executable. 24 | 25 | ```shell 26 | chmod +x tgcf_start.sh 27 | ``` 28 | 29 | **Step 2.** 30 | Create a service. 31 | 32 | Create a file named `tgcf.service` and put the following content into it. 33 | 34 | ```ini 35 | [Unit] 36 | Description=The ultimate tool to automate custom telegram message forwarding. 37 | After=network.target 38 | 39 | [Install] 40 | WantedBy=multi-user.target 41 | 42 | [Service] 43 | Type=simple 44 | ExecStart=/home/aahnik/tgcf_start.sh 45 | # use the absolute path of the shell script in your server 46 | Restart=always 47 | RestartSec=5 48 | StandardOutput=syslog 49 | StandardError=syslog 50 | SyslogIdentifier=%n 51 | 52 | ``` 53 | 54 | **Step 3.** 55 | Install and enable the service. 56 | 57 | ```shell 58 | sudo mv tgcf.service /etc/systemd/system 59 | sudo systemctl daemon-reload 60 | sudo systemctl enable tgcf.service 61 | ``` 62 | 63 | 64 | ## Running via `systemctl` 65 | 66 | Now to **start tgcf** using systemctl you can simply do 67 | 68 | ```shell 69 | sudo systemctl start tgcf 70 | ``` 71 | 72 | You can also **see the status** of the service by running 73 | 74 | ```shell 75 | sudo systemctl status tgcf 76 | ``` 77 | 78 | To **see the live logs** 79 | 80 | ```shell 81 | journalctl -f -u tgcf 82 | ``` 83 | 84 | To **stop the service** 85 | 86 | ```shell 87 | sudo systemctl stop tgcf 88 | ``` 89 | 90 | 91 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Login-with-a-bot-or-user-account.md: -------------------------------------------------------------------------------- 1 | 2 | In Telegram, there are two types of accounts: Users and Bots. With `tgcf` you may use your own user account or a bot that you have created. 3 | 4 |
5 | Read this if you are planning to use a bot account 6 |
7 | 8 | - You can create a bot from [@BotFather](https://telegram.me/BotFather) 9 | - The bot must be added to the groups and channels (both source and destination) 10 | - The privacy mode of bots should be set to off. That means the bot should be allowed to listen to all messages in the channels/groups it is a member of. By default this setting is `on`, you have to turn privacy mode `off` from BotFather. 11 | 12 |
13 | 14 | ## Default behavior 15 | 16 | When you run `tgcf` for the first time, it will interactively prompt you to enter your phone number or bot token. A session file will be generated and saved in the folder from which you ran `tgcf`. 17 | 18 | When you will run `tgcf` again, from the same folder, you will not be required to log in. 19 | 20 | ## Generate Session String 21 | 22 | 23 | ### Run Online 24 | 25 | Click on the below button to run in a free repl. 26 | 27 | [![run on repl](https://docs.replit.com/images/repls/run-on-replit.svg)](https://replit.com/@aahnik/tg-login) 28 | 29 | - The session string will not be printed on the screen. (for security purposes) 30 | - The session string will be securely saved in your Saved Messages (if you log in with your own user account). 31 | - The session string will be sent to you (if you log in with a bot account). 32 | - All sensitive user input in the repl is made invisible to ensure high security. 33 | 34 | ### Run on your machine 35 | 36 | - Open your terminal in Mac/Windows/Linux/Android 37 | - Make sure you have `python` installed. 38 | If you don't have python: 39 | - for Linux/Mac, its generally already installed. 40 | - for windows install python 3.8 or above from the Microsoft store 41 | - for android (termux) run `pkg install python` 42 | 43 | - Install `tg-login` by running `pip install tg-login` 44 | - Run `tg-login` 45 | - It will prompt you to enter your details, and then print your session string on the screen. 46 | - Copy the session string, and never share it with anyone. 47 | 48 | 49 | -------------------------------------------------------------------------------- /tgcf/web_ui/pages/6_🔬_Advanced.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import streamlit as st 4 | 5 | from tgcf.config import CONFIG_FILE_NAME, read_config, write_config 6 | from tgcf.utils import platform_info 7 | from tgcf.web_ui.password import check_password 8 | from tgcf.web_ui.utils import hide_st, switch_theme 9 | 10 | CONFIG = read_config() 11 | 12 | st.set_page_config( 13 | page_title="Advanced", 14 | page_icon="🔬", 15 | ) 16 | hide_st(st) 17 | switch_theme(st,CONFIG) 18 | 19 | if check_password(st): 20 | 21 | st.warning("This page is for developers and advanced users.") 22 | if st.checkbox("I agree"): 23 | 24 | with st.expander("Version & Platform"): 25 | st.code(platform_info()) 26 | 27 | with st.expander("Configuration"): 28 | with open(CONFIG_FILE_NAME, "r") as file: 29 | data = json.loads(file.read()) 30 | dumped = json.dumps(data, indent=3) 31 | st.download_button( 32 | f"Download config json", data=dumped, file_name=CONFIG_FILE_NAME 33 | ) 34 | st.json(data) 35 | 36 | with st.expander("Special Options for Live Mode"): 37 | CONFIG.live.sequential_updates = st.checkbox( 38 | "Enforce sequential updates", value=CONFIG.live.sequential_updates 39 | ) 40 | 41 | CONFIG.live.delete_on_edit = st.text_input( 42 | "Delete a message when source edited to", 43 | value=CONFIG.live.delete_on_edit, 44 | ) 45 | st.write( 46 | "When you edit the message in source to something particular, the message will be deleted in both source and destinations." 47 | ) 48 | if st.checkbox("Customize Bot Messages"): 49 | st.info( 50 | "Note: For userbots, the commands start with `.` instead of `/`, like `.start` and not `/start`" 51 | ) 52 | CONFIG.bot_messages.start = st.text_area( 53 | "Bot's Reply to /start command", value=CONFIG.bot_messages.start 54 | ) 55 | CONFIG.bot_messages.bot_help = st.text_area( 56 | "Bot's Reply to /help command", value=CONFIG.bot_messages.bot_help 57 | ) 58 | 59 | if st.button("Save"): 60 | write_config(CONFIG) 61 | -------------------------------------------------------------------------------- /tgcf/web_ui/pages/1_🔑_Telegram_Login.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | from tgcf.config import CONFIG, read_config, write_config 4 | from tgcf.web_ui.password import check_password 5 | from tgcf.web_ui.utils import hide_st, switch_theme 6 | 7 | CONFIG = read_config() 8 | 9 | st.set_page_config( 10 | page_title="Telegram Login", 11 | page_icon="🔑", 12 | ) 13 | hide_st(st) 14 | switch_theme(st,CONFIG) 15 | if check_password(st): 16 | CONFIG.login.API_ID = int( 17 | st.text_input("API ID", value=str(CONFIG.login.API_ID), type="password") 18 | ) 19 | CONFIG.login.API_HASH = st.text_input( 20 | "API HASH", value=CONFIG.login.API_HASH, type="password" 21 | ) 22 | st.write("You can get api id and api hash from https://my.telegram.org.") 23 | 24 | user_type = st.radio( 25 | "Choose account type", ["Bot", "User"], index=CONFIG.login.user_type 26 | ) 27 | if user_type == "Bot": 28 | CONFIG.login.user_type = 0 29 | CONFIG.login.BOT_TOKEN = st.text_input( 30 | "Enter bot token", value=CONFIG.login.BOT_TOKEN, type="password" 31 | ) 32 | else: 33 | CONFIG.login.user_type = 1 34 | CONFIG.login.SESSION_STRING = st.text_input( 35 | "Enter session string", value=CONFIG.login.SESSION_STRING, type="password" 36 | ) 37 | with st.expander("How to get session string ?"): 38 | st.markdown( 39 | """ 40 | 41 | Link to repl: https://replit.com/@aahnik/tg-login?v=1 42 | 43 | _Click on the above link and enter api id, api hash, and phone no to generate session string._ 44 | 45 | **Note from developer:** 46 | 47 | Due some issues logging in with a user account using a phone no is not supported in this web interface. 48 | 49 | I have built a command-line program named tg-login (https://github.com/aahnik/tg-login) that can generate the session string for you. 50 | 51 | You can run tg-login on your computer, or securely in this repl. tg-login is open source, and you can also inspect the bash script running in the repl. 52 | 53 | What is a session string ? 54 | https://docs.telethon.dev/en/stable/concepts/sessions.html#string-sessions 55 | 56 | """ 57 | ) 58 | 59 | if st.button("Save"): 60 | write_config(CONFIG) 61 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Environment-Variables.md: -------------------------------------------------------------------------------- 1 | An environment variable is a dynamic-named value that can affect the way running processes will behave on a computer. They are part of the environment in which a process runs. 2 | 3 | The secret credentials like `API_ID` and `API_HASH` are stored as environment variables. 4 | 5 | ## All env vars 6 | 7 | | Env Var | Value | Requirement | 8 | | ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 9 | | **`API_ID`** | obtain it from [my.telegram.org](https://my.telegram.org) | always required | 10 | | **`API_HASH`** | obtain it from [my.telegram.org](https://my.telegram.org) | always required | 11 | | `TGCF_MODE` | [`past` or `live`](https://github.com/aahnik/tgcf/wiki/Past-vs-Live-modes-explained) | only required if you don't have interactive shell while running `tgcf`. | 12 | | `BOT_TOKEN` | obtained from [@BotFather](https://telegram.me/BotFather) | required if you are running`tgcf`with a bot account. | 13 | | `SESSION_STRING` | obtained after [login](https://github.com/aahnik/tgcf/wiki/Login-with-a-bot-or-user-account#generate-session-string) | only required if you are using `tgcf`with user account. | 14 | | `TGCF_CONFIG` | contents of [`tgcf.config.yml`](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) | only required if you cant edit files in your cloud deploy (digital ocean app or heroku dyno) | 15 | 16 | 17 | 18 | 19 | ## Setting env vars 20 | 21 | There are various methods to set env vars 22 | 23 | ### `.env` File 24 | 25 | You can easily set environment variables for `tgcf` using a `.env` file in the directory from which `tgcf` is invoked. 26 | 27 | ```shell 28 | API_ID=543213 29 | API_HASH=uihfuiwruiw28490238huawfiuhf 30 | # put your real values here 31 | ``` 32 | 33 | ### Cloud Deploys 34 | 35 | When you are deploying to a cloud platform, and you cant create files (Heroku or digital ocean apps), you can set environment variables using the GUI provided by the platforms. Please read platform-specific guides in the wiki for more details. 36 | 37 | When you are deploying on a cloud platform, you can configure tgcf using environment variables. The contents of `tgcf.config.yml` can be put inside the environment variable called `TGCF_CONFIG`. -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Deploy-to-Heroku.md: -------------------------------------------------------------------------------- 1 | Heroku is a cloud platform that lets companies build, deliver, monitor, and scale apps, it is one of the fastest ways to go from idea to URL, bypassing all those infrastructure headaches. 2 | 3 | You can deploy `tgcf` to Heroku very easily. 4 | 5 | ## Limitations 6 | 7 | - Heroku has an ephemeral file system. 8 | - Thus you cant store your files here. 9 | - `tgcf.config.yml` can't be created here. 10 | - Instead, you can use an environment variable named `TGCF_CONFIG` to store the contents of the configuration file. 11 | - `tgcf` in past mode won't work properly in Heroku, as the environment variable TGCF_CONFIG can't be updated. 12 | 13 | ## Pros 14 | 15 | - `tgcf` will work **perfectly fine** in `live` mode in Heroku. 16 | - Heroku offers a great free tier of 450 hrs/mo 17 | 18 | ## One-click deploy 19 | 20 | 1. Make sure you have a Heroku account and then click on this button. 21 | 22 | Deploy to Heroku 23 | 24 | 2. A Heroku page will open where you can set all the environment variables. 25 | 26 | - Set the name of the app whatever you want. 27 | 28 | ![image](https://user-images.githubusercontent.com/66209958/115880520-7287f980-a468-11eb-9bfc-5a72cbe668d9.png) 29 | 30 | - Set your API ID and API HASH obtained from [my.telegram.org](https://my.telegram.org). Set the mode to `live`. 31 | 32 | 33 | - You may keep your `SESSION_STRING` and `TGCF_CONFIG` empty for now. 34 | 35 | 36 | - Now click the deploy app button. 37 | 38 | 39 | 3. It will take some time to build and deploy. After the deployment is complete, click on the manage app button. 40 | ![image](https://user-images.githubusercontent.com/66209958/115881849-cb0bc680-a469-11eb-8b35-6bf5c6a5eca4.png) 41 | 42 | 4. How to get the session string? [Read this](https://github.com/aahnik/tgcf/wiki/Login-with-a-bot-or-user-account#generate-session-string). 43 | 44 | 5. Now go to the settings tab and click Reveal config vars. Click on the pencil button for the session string and config var, and then paste the session string the value of that. 45 | 46 | 6. Learn [how to configure tgcf](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F), and then write your configuration in the `TGCF_CONFIG` env var. 47 | 48 | 7. Go to the resources tab, and turn on the worker and click confirm. 49 | 50 | ![image](https://user-images.githubusercontent.com/66209958/115882913-dc090780-a46a-11eb-980b-6b0f49ff45f5.png) -------------------------------------------------------------------------------- /tgcf/plugin_models.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, Dict, List 3 | 4 | from pydantic import BaseModel 5 | from watermark import Position 6 | 7 | 8 | class FileType(str, Enum): 9 | AUDIO = "audio" 10 | GIF = "gif" 11 | VIDEO = "video" 12 | VIDEO_NOTE = "video_note" 13 | STICKER = "sticker" 14 | CONTACT = "contact" 15 | PHOTO = "photo" 16 | DOCUMENT = "document" 17 | NOFILE = "nofile" 18 | 19 | 20 | class FilterList(BaseModel): 21 | blacklist: List[str] = [] 22 | whitelist: List[str] = [] 23 | 24 | 25 | class FilesFilterList(BaseModel): 26 | blacklist: List[FileType] = [] 27 | whitelist: List[FileType] = [] 28 | 29 | 30 | class TextFilter(FilterList): 31 | case_sensitive: bool = False 32 | regex: bool = False 33 | 34 | 35 | class Style(str, Enum): 36 | BOLD = "bold" 37 | ITALICS = "italics" 38 | CODE = "code" 39 | STRIKE = "strike" 40 | PLAIN = "plain" 41 | PRESERVE = "preserve" 42 | 43 | 44 | STYLE_CODES = {"bold": "**", "italics": "__", "code": "`", "strike": "~~", "plain": ""} 45 | 46 | # define plugin configs 47 | 48 | 49 | class Filters(BaseModel): 50 | check: bool = False 51 | users: FilterList = FilterList() 52 | files: FilesFilterList = FilesFilterList() 53 | text: TextFilter = TextFilter() 54 | 55 | 56 | class Format(BaseModel): 57 | check: bool = False 58 | style: Style = Style.PRESERVE 59 | 60 | 61 | class MarkConfig(BaseModel): 62 | check: bool = False 63 | image: str = "image.png" 64 | position: Position = Position.centre 65 | frame_rate: int = 15 66 | 67 | 68 | class OcrConfig(BaseModel): 69 | check: bool = False 70 | 71 | 72 | class Replace(BaseModel): 73 | check: bool = False 74 | text: Dict[str, str] = {} 75 | text_raw: str = "" 76 | regex: bool = False 77 | 78 | 79 | class Caption(BaseModel): 80 | check: bool = False 81 | header: str = "" 82 | footer: str = "" 83 | 84 | class Sender(BaseModel): 85 | check: bool = False 86 | user_type: int = 0 # 0:bot, 1:user 87 | BOT_TOKEN: str = "" 88 | SESSION_STRING: str = "" 89 | 90 | class PluginConfig(BaseModel): 91 | filter: Filters = Filters() 92 | fmt: Format = Format() 93 | mark: MarkConfig = MarkConfig() 94 | ocr: OcrConfig = OcrConfig() 95 | replace: Replace = Replace() 96 | caption: Caption = Caption() 97 | sender: Sender = Sender() 98 | 99 | 100 | # List of plugins that need to load asynchronously 101 | ASYNC_PLUGIN_IDS = ['sender'] -------------------------------------------------------------------------------- /tgcf/web_ui/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, List 3 | from run import package_dir 4 | from streamlit.components.v1 import html 5 | from tgcf.config import write_config 6 | 7 | 8 | def get_list(string: str): 9 | # string where each line is one element 10 | my_list = [] 11 | for line in string.splitlines(): 12 | clean_line = line.strip() 13 | if clean_line != "": 14 | my_list.append(clean_line) 15 | return my_list 16 | 17 | 18 | def get_string(my_list: List): 19 | string = "" 20 | for item in my_list: 21 | string += f"{item}\n" 22 | return string 23 | 24 | 25 | def dict_to_list(dict: Dict): 26 | my_list = [] 27 | for key, val in dict.items(): 28 | my_list.append(f"{key}: {val}") 29 | return my_list 30 | 31 | 32 | def list_to_dict(my_list: List): 33 | my_dict = {} 34 | for item in my_list: 35 | key, val = item.split(":") 36 | my_dict[key.strip()] = val.strip() 37 | return my_dict 38 | 39 | 40 | def apply_theme(st,CONFIG,hidden_container): 41 | """Apply theme using browser's local storage""" 42 | if st.session_state.theme == '☀️': 43 | theme = 'Light' 44 | CONFIG.theme = 'light' 45 | else: 46 | theme = 'Dark' 47 | CONFIG.theme = 'dark' 48 | write_config(CONFIG) 49 | script = f"' 54 | with hidden_container: # prevents the layout from shifting 55 | html(script,height=0,width=0) 56 | 57 | 58 | def switch_theme(st,CONFIG): 59 | """Display the option to change theme (Light/Dark)""" 60 | with st.sidebar: 61 | leftpad,content,rightpad = st.columns([0.27,0.46,0.27]) 62 | with content: 63 | st.radio ( 64 | 'Theme:',['☀️','🌒'], 65 | horizontal=True, 66 | label_visibility="collapsed", 67 | index=CONFIG.theme == 'dark', 68 | on_change=apply_theme, 69 | key="theme", 70 | args=[st,CONFIG,leftpad] # or rightpad 71 | ) 72 | 73 | 74 | def hide_st(st): 75 | dev = os.getenv("DEV") 76 | if dev: 77 | return 78 | hide_streamlit_style = """ 79 | 83 | """ 84 | st.markdown(hide_streamlit_style, unsafe_allow_html=True) 85 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '35 5 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /tgcf/plugins/filter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Dict, List 3 | 4 | from pydantic import BaseModel # pylint: disable=no-name-in-module 5 | 6 | from tgcf.plugin_models import FileType, Filters, TextFilter 7 | from tgcf.plugins import TgcfMessage, TgcfPlugin 8 | from tgcf.utils import match 9 | 10 | 11 | class TgcfFilter(TgcfPlugin): 12 | id_ = "filter" 13 | 14 | def __init__(self, data) -> None: 15 | self.filters = data 16 | self.case_correct() 17 | logging.info(self.filters) 18 | 19 | def case_correct(self) -> None: 20 | textf: TextFilter = self.filters.text 21 | 22 | if textf.case_sensitive is False and textf.regex is False: 23 | textf.blacklist = [item.lower() for item in textf.blacklist] 24 | textf.whitelist = [item.lower() for item in textf.whitelist] 25 | 26 | def modify(self, tm: TgcfMessage) -> TgcfMessage: 27 | 28 | if self.users_safe(tm): 29 | logging.info("Message passed users filter") 30 | if self.files_safe(tm): 31 | logging.info("Message passed files filter") 32 | if self.text_safe(tm): 33 | logging.info("Message passed text filter") 34 | return tm 35 | 36 | def text_safe(self, tm: TgcfMessage) -> bool: 37 | flist = self.filters.text 38 | 39 | text = tm.text 40 | if not flist.case_sensitive: 41 | text = text.lower() 42 | if not text and flist.whitelist == []: 43 | return True 44 | 45 | # first check if any blacklisted pattern is present 46 | for forbidden in flist.blacklist: 47 | if match(forbidden, text, self.filters.text.regex): 48 | return False # when a forbidden pattern is found 49 | 50 | if not flist.whitelist: 51 | return True # if no whitelist is present 52 | 53 | # if whitelist is present 54 | for allowed in flist.whitelist: 55 | if match(allowed, text, self.filters.text.regex): 56 | return True # only when atleast one whitelisted pattern is found 57 | 58 | def users_safe(self, tm: TgcfMessage) -> bool: 59 | flist = self.filters.users 60 | sender = str(tm.sender_id) 61 | if sender in flist.blacklist: 62 | return False 63 | if not flist.whitelist: 64 | return True 65 | if sender in flist.whitelist: 66 | return True 67 | 68 | def files_safe(self, tm: TgcfMessage) -> bool: 69 | flist = self.filters.files 70 | fl_type = tm.file_type 71 | if fl_type in flist.blacklist: 72 | return False 73 | if not flist.whitelist: 74 | return True 75 | if fl_type in flist.whitelist: 76 | return True 77 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/How-to-configure-tgcf-?.md: -------------------------------------------------------------------------------- 1 | The application `tgcf` is configured using a simple `YAML` file. If you are new to the `YAML` syntax, don't worry, it's easy, and you will learn it. 2 | 3 | ## Introducing YAML 4 | 5 | Tutorials on YAML syntax: 6 | - [Article by Tutorial Point](https://www.tutorialspoint.com/yaml/yaml_basics.htm) 7 | - [Article by W3 Schools](https://www.w3schools.io/file/yaml-cheatsheet-syntax) 8 | - [YouTube video by Nana](https://youtu.be/1uFVr15xDGg?t=73) 9 | 10 | ## Where to write 11 | 12 | You may write your configuration in the `tgcf.config.yml` file. (when running on your own computer) 13 | 14 | When you are deploying on a cloud platform where you can't edit files, you may configure tgcf using environment variables. The contents of `tgcf.config.yml` can be put inside the environment variable called `TGCF_CONFIG`. Read the wiki for platform-specific guides on how to set environment variables in different platforms. 15 | 16 | 17 | ## Example Configuration 18 | 19 | - For the `source` and `dest` fields use the username of the channel/bot/person/group. (omit the `@` symbol at the start). 20 | - If the private entity does not have a username, you may use the link of the private channel/group. 21 | 22 | Below is an example configuration. Don't copy-paste this. Understand what each part does. 23 | 24 | ```yaml 25 | admins: [yourUserName,AnotherPerson] 26 | # when tgcf is run in live mode, the admins can run commands to change the configuration 27 | 28 | forwards: 29 | - source: channelName 30 | dest: [anotherChannel,https://t.me/channelLink] 31 | # use username or link of the entity 32 | 33 | 34 | show_forwarded_from: false 35 | 36 | plugins: 37 | filter: 38 | text: 39 | blacklist: ["nope"] 40 | replace: 41 | text: 42 | god: devil 43 | tokyo: delhi 44 | 45 | ``` 46 | 47 | ## Schema 48 | 49 | Here is the complete schema for the configuration file. 50 | 51 | - `admins` (the list of usernames or ids of the admins) 52 | > - setting admins is not compulsory 53 | > - if no admins are set, and you run tgcf in live mode, then no one can run commands to change the configuration. 54 | > - the bot/user bot **will work perfectly fine** as per your configuration file 55 | - `forwards` (a list of forward objects) 56 | - forward ( contains a `source` (string), a `dest` (list of strings) and an `offset`(optional integer) ) 57 | - `show_forwarded_from` (boolean: true/false) 58 | - `live` 59 | - `delete_sync` : bool (true or false) 60 | - `past` 61 | - `delay`: int (between 1 to 100 )(seconds) (time to wait after every message is sent) 62 | 63 | 64 | - `plugins` contain the name of the plugin and the data to be passed to that plugin. 65 | - What data to pass to plugins? is defined in the documentation for that plugin. Here is the [list of all plugins](https://github.com/aahnik/tgcf/wiki/Plugins). 66 | 67 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Home.md: -------------------------------------------------------------------------------- 1 | Welcome to the tgcf wiki! 2 | 3 | | [Overview](https://github.com/aahnik/tgcf#readme) | [Roadmap](https://github.com/aahnik/tgcf/discussions/43) | [Support](https://github.com/aahnik/tgcf/discussions/2) | [FAQs](https://github.com/aahnik/tgcf/discussions/196) | 4 | | -------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------ | 5 | 6 | 7 |
8 | Platforms 9 |
10 | 11 | - [Run tgcf on Windows](https://github.com/aahnik/tgcf/wiki/Run-tgcf-on-Windows) 12 | - [Run on Android using Termux](https://github.com/aahnik/tgcf/wiki/Run-on-Android-using-Termux) 13 | - [Install and run using docker](https://github.com/aahnik/tgcf/wiki/Install-and-run-using-docker) 14 | 15 |
16 | 17 |
18 | Basics 19 |
20 | 21 | - [Past vs Live modes explained](https://github.com/aahnik/tgcf/wiki/Past-vs-Live-modes-explained) 22 | - [Environment Variables](https://github.com/aahnik/tgcf/wiki/Environment-Variables) 23 | - [How to configure tgcf ?](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) 24 | - [CLI Usage](https://github.com/aahnik/tgcf/wiki/CLI-Usage) 25 | - [Running continuously in background](https://github.com/aahnik/tgcf/discussions/219#discussioncomment-920558) 26 | 27 |
28 | 29 |
30 | Deploy to cloud 31 |
32 | 33 | - [Deploy to Heroku](https://github.com/aahnik/tgcf/wiki/Deploy-to-Heroku) 34 | - [Deploy to Digital Ocean](https://github.com/aahnik/tgcf/wiki/Deploy-to-Digital-Ocean) 35 | - [Run for free on Gitpod](https://github.com/aahnik/tgcf/wiki/Run-for-free-on-Gitpod) 36 | - [Run tgcf in past mode periodically using GitHub Actions](https://github.com/aahnik/tgcf/wiki/Run-tgcf-in-past-mode-periodically) 37 | 38 |
39 | 40 |
41 | Plugins 42 |
43 | 44 | - [Intro](https://github.com/aahnik/tgcf/wiki/Plugins) 45 | - [How to use filters ?](https://github.com/aahnik/tgcf/wiki/How-to-use-filters-%3F) 46 | - [Format text before sending to destination](https://github.com/aahnik/tgcf/wiki/Format-text-before-sending-to-destination) 47 | - [Text Replacement feature explained](https://github.com/aahnik/tgcf/wiki/Text-Replacement-feature-explained) 48 | - [How to use watermarking ?](https://github.com/aahnik/tgcf/wiki/How-to-use--watermarking-%3F) 49 | - [You can do OCR !](https://github.com/aahnik/tgcf/wiki/You-can-do-OCR) 50 | 51 |
52 | 53 | 54 |
55 | Development 56 |
57 | 58 | - [How to write a plugin for tgcf ?](https://github.com/aahnik/tgcf/wiki/How-to-write-a-plugin-for-tgcf-%3F) 59 | - [Contributing Guidelines](https://github.com/aahnik/tgcf/blob/main/.github/CONTRIBUTING.md#contributing-guidelines) 60 | - [Package management with Poetry](https://python-poetry.org/docs/) 61 | - [Telethon documentation](https://docs.telethon.dev/en/latest/) 62 |
-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.session 2 | *.session-journal 3 | t.py 4 | tgcf.config.yml 5 | .vscode 6 | foo.py 7 | bar.py 8 | foo.bar 9 | t.ipynb 10 | tgcf_external.py 11 | t.yml 12 | test.py 13 | tgcf.config.json 14 | *.txt 15 | todo 16 | *.ipynb 17 | 18 | 19 | 20 | # Byte-compiled / optimized / DLL files 21 | __pycache__/ 22 | *.py[cod] 23 | *$py.class 24 | 25 | # C extensions 26 | *.so 27 | 28 | # Distribution / packaging 29 | .Python 30 | build/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | .eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | wheels/ 42 | share/python-wheels/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | MANIFEST 47 | 48 | # PyInstaller 49 | # Usually these files are written by a python script from a template 50 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 51 | *.manifest 52 | *.spec 53 | 54 | # Installer logs 55 | pip-log.txt 56 | pip-delete-this-directory.txt 57 | 58 | # Unit test / coverage reports 59 | htmlcov/ 60 | .tox/ 61 | .nox/ 62 | .coverage 63 | .coverage.* 64 | .cache 65 | nosetests.xml 66 | coverage.xml 67 | *.cover 68 | *.py,cover 69 | .hypothesis/ 70 | .pytest_cache/ 71 | cover/ 72 | 73 | # Translations 74 | *.mo 75 | *.pot 76 | 77 | # Django stuff: 78 | *.log 79 | local_settings.py 80 | db.sqlite3 81 | db.sqlite3-journal 82 | 83 | # Flask stuff: 84 | instance/ 85 | .webassets-cache 86 | 87 | # Scrapy stuff: 88 | .scrapy 89 | 90 | # Sphinx documentation 91 | docs/_build/ 92 | 93 | # PyBuilder 94 | .pybuilder/ 95 | target/ 96 | 97 | # Jupyter Notebook 98 | .ipynb_checkpoints 99 | 100 | # IPython 101 | profile_default/ 102 | ipython_config.py 103 | 104 | # pyenv 105 | # For a library or package, you might want to ignore these files since the code is 106 | # intended to run in multiple environments; otherwise, check them in: 107 | # .python-version 108 | 109 | # pipenv 110 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 111 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 112 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 113 | # install all needed dependencies. 114 | #Pipfile.lock 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | -------------------------------------------------------------------------------- /.github/archives/tgcf_0.2_wiki/Run-tgcf-on-Windows.md: -------------------------------------------------------------------------------- 1 | Windows is a mess! In Linux or Mac, a terminal and python are generally pre-installed. But that's not the case with windows. 2 | 3 | Here goes the complete guide to set up and use `tgcf` on a Windows machine. 4 | 5 | ## Pre-requisites 6 | 7 | 1. Open Microsoft Store 8 | 9 | ![image](https://user-images.githubusercontent.com/66209958/115837680-7a2eaa80-a436-11eb-9cca-11e12694e8b3.png) 10 | 11 | 2. Install [Powershell]() , [Windows Terminal](), and Python 3.9 12 | 13 | 14 | ![image](https://user-images.githubusercontent.com/66209958/115838965-d0e8b400-a437-11eb-818a-652951ae44ee.png) 15 | 16 | ![image](https://user-images.githubusercontent.com/66209958/115839446-49e80b80-a438-11eb-9149-b93d6218e0dc.png) 17 | 18 | ![image](https://user-images.githubusercontent.com/66209958/115839540-608e6280-a438-11eb-91e6-9285cc6301ee.png) 19 | 20 | 21 | ![image](https://user-images.githubusercontent.com/66209958/115959754-644ee180-a52b-11eb-8ff7-a55692beb853.png) 22 | 23 | 3. By default windows has Notepad. Its a horrible text editor. Windows file explorer is also shitty. It appends `.txt` to every text file. For a better experience, Install VS Code from [code.visualstudio.com](https://code.visualstudio.com/) for easy editing of text files. 24 | When you will be writing the `tgcf.config.yml` VS code will automatically provide syntax highlighting and type checking. 25 | ![Screenshot (7)](https://user-images.githubusercontent.com/66209958/115840953-e4951a00-a439-11eb-9db4-b87733e2dd98.png) 26 | 27 | ## Install tgcf 28 | 29 | Open Powershell in Windows Terminal and run pip install tgcf 30 | 31 | ![image](https://user-images.githubusercontent.com/66209958/115841408-6127f880-a43a-11eb-92fd-215ab3a4c8aa.png) 32 | 33 | 34 | ## Configure and Run 35 | 36 | 1. You should create a new folder to store `tgcf` configuration files. Every time you run `tgcf` you should run from inside this folder. 37 | 38 | 2. Open the folder with VS Code and create the files `.env` and `config.tgcf.yml`. 39 | You will be required to login to your Telegram account only for the first time. The session files will be stored in this folder. Don't delete them, and keep them secret. 40 | , go inside it, create .env and tgcf.config.yml, run tgcf 41 | ![Screenshot (12)](https://user-images.githubusercontent.com/66209958/115847554-b5ce7200-a440-11eb-93e0-55de40a611e5.png) 42 | ![Screenshot (13)](https://user-images.githubusercontent.com/66209958/115847567-b8c96280-a440-11eb-8540-34dd89c273c9.png) 43 | ![Screenshot (14)](https://user-images.githubusercontent.com/66209958/115847578-bbc45300-a440-11eb-8dff-6e9f163885ba.png) 44 | ![Screenshot (15)](https://user-images.githubusercontent.com/66209958/115847590-be26ad00-a440-11eb-9879-b78cabef0d2d.png) 45 | ![Screenshot (17)](https://user-images.githubusercontent.com/66209958/115847693-d5659a80-a440-11eb-9e3e-fcdff16c3c97.png) 46 | 47 | 3. Open terminal in VS Code and run tgcf 48 | 49 | ![Screenshot (19)](https://user-images.githubusercontent.com/66209958/115848550-9f74e600-a441-11eb-92bb-ee014a9639c7.png) 50 | ![Screenshot (20)](https://user-images.githubusercontent.com/66209958/115848561-a1d74000-a441-11eb-87c7-731be1bcbca9.png) 51 | 52 | Every time, run tgcf from the same folder. 53 | 54 | -------------------------------------------------------------------------------- /tgcf/utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions to smoothen your life.""" 2 | 3 | import logging 4 | import os 5 | import platform 6 | import re 7 | import sys 8 | from datetime import datetime 9 | from typing import TYPE_CHECKING 10 | 11 | from telethon.client import TelegramClient 12 | from telethon.hints import EntityLike 13 | from telethon.tl.custom.message import Message 14 | 15 | from tgcf import __version__ 16 | from tgcf.config import CONFIG 17 | from tgcf.plugin_models import STYLE_CODES 18 | 19 | if TYPE_CHECKING: 20 | from tgcf.plugins import TgcfMessage 21 | 22 | 23 | def platform_info(): 24 | nl = "\n" 25 | return f"""Running tgcf {__version__}\ 26 | \nPython {sys.version.replace(nl,"")}\ 27 | \nOS {os.name}\ 28 | \nPlatform {platform.system()} {platform.release()}\ 29 | \n{platform.architecture()} {platform.processor()}""" 30 | 31 | 32 | async def send_message(recipient: EntityLike, tm: "TgcfMessage") -> Message: 33 | """Forward or send a copy, depending on config.""" 34 | client: TelegramClient = tm.client 35 | if CONFIG.show_forwarded_from: 36 | return await client.forward_messages(recipient, tm.message) 37 | if tm.new_file: 38 | message = await client.send_file( 39 | recipient, tm.new_file, caption=tm.text, reply_to=tm.reply_to 40 | ) 41 | return message 42 | tm.message.text = tm.text 43 | return await client.send_message(recipient, tm.message, reply_to=tm.reply_to) 44 | 45 | 46 | def cleanup(*files: str) -> None: 47 | """Delete the file names passed as args.""" 48 | for file in files: 49 | try: 50 | os.remove(file) 51 | except FileNotFoundError: 52 | logging.info(f"File {file} does not exist, so cant delete it.") 53 | 54 | 55 | def stamp(file: str, user: str) -> str: 56 | """Stamp the filename with the datetime, and user info.""" 57 | now = str(datetime.now()) 58 | outf = safe_name(f"{user} {now} {file}") 59 | try: 60 | os.rename(file, outf) 61 | return outf 62 | except Exception as err: 63 | logging.warning(f"Stamping file name failed for {file} to {outf}. \n {err}") 64 | 65 | 66 | def safe_name(string: str) -> str: 67 | """Return safe file name. 68 | 69 | Certain characters in the file name can cause potential problems in rare scenarios. 70 | """ 71 | return re.sub(pattern=r"[-!@#$%^&*()\s]", repl="_", string=string) 72 | 73 | 74 | def match(pattern: str, string: str, regex: bool) -> bool: 75 | if regex: 76 | return bool(re.findall(pattern, string)) 77 | return pattern in string 78 | 79 | 80 | def replace(pattern: str, new: str, string: str, regex: bool) -> str: 81 | def fmt_repl(matched): 82 | style = new 83 | s = STYLE_CODES.get(style) 84 | return f"{s}{matched.group(0)}{s}" 85 | 86 | if regex: 87 | if new in STYLE_CODES: 88 | compliled_pattern = re.compile(pattern) 89 | return compliled_pattern.sub(repl=fmt_repl, string=string) 90 | return re.sub(pattern, new, string) 91 | else: 92 | return string.replace(pattern, new) 93 | 94 | 95 | def clean_session_files(): 96 | for item in os.listdir(): 97 | if item.endswith(".session") or item.endswith(".session-journal"): 98 | os.remove(item) 99 | -------------------------------------------------------------------------------- /tgcf/cli.py: -------------------------------------------------------------------------------- 1 | """This module implements the command line interface for tgcf.""" 2 | 3 | import asyncio 4 | import logging 5 | import os 6 | import sys 7 | from enum import Enum 8 | from typing import Optional 9 | 10 | import typer 11 | from dotenv import load_dotenv 12 | from rich import console, traceback 13 | from rich.logging import RichHandler 14 | from verlat import latest_release 15 | 16 | from tgcf import __version__ 17 | 18 | load_dotenv(".env") 19 | 20 | FAKE = bool(os.getenv("FAKE")) 21 | app = typer.Typer(add_completion=False) 22 | 23 | con = console.Console() 24 | 25 | 26 | def topper(): 27 | print("tgcf") 28 | version_check() 29 | print("\n") 30 | 31 | 32 | class Mode(str, Enum): 33 | """tgcf works in two modes.""" 34 | 35 | PAST = "past" 36 | LIVE = "live" 37 | 38 | 39 | def verbosity_callback(value: bool): 40 | """Set logging level.""" 41 | traceback.install() 42 | if value: 43 | level = logging.INFO 44 | else: 45 | level = logging.WARNING 46 | logging.basicConfig( 47 | level=level, 48 | format="%(message)s", 49 | handlers=[ 50 | RichHandler( 51 | rich_tracebacks=True, 52 | markup=True, 53 | ) 54 | ], 55 | ) 56 | topper() 57 | logging.info("Verbosity turned on! This is suitable for debugging") 58 | 59 | 60 | def version_callback(value: bool): 61 | """Show current version and exit.""" 62 | 63 | if value: 64 | con.print(__version__) 65 | raise typer.Exit() 66 | 67 | 68 | def version_check(): 69 | latver = latest_release("tgcf").version 70 | if __version__ != latver: 71 | con.print( 72 | f"tgcf has a newer release {latver} availaible!\ 73 | \nVisit http://bit.ly/update-tgcf", 74 | style="bold yellow", 75 | ) 76 | else: 77 | con.print(f"Running latest tgcf version {__version__}", style="bold green") 78 | 79 | 80 | @app.command() 81 | def main( 82 | mode: Mode = typer.Argument( 83 | ..., help="Choose the mode in which you want to run tgcf.", envvar="TGCF_MODE" 84 | ), 85 | verbose: Optional[bool] = typer.Option( # pylint: disable=unused-argument 86 | None, 87 | "--loud", 88 | "-l", 89 | callback=verbosity_callback, 90 | envvar="LOUD", 91 | help="Increase output verbosity.", 92 | ), 93 | version: Optional[bool] = typer.Option( # pylint: disable=unused-argument 94 | None, 95 | "--version", 96 | "-v", 97 | callback=version_callback, 98 | help="Show version and exit.", 99 | ), 100 | ): 101 | """The ultimate tool to automate custom telegram message forwarding. 102 | 103 | Source Code: https://github.com/aahnik/tgcf 104 | 105 | For updates join telegram channel @aahniks_code 106 | 107 | To run web interface run `tgcf-web` command. 108 | """ 109 | if FAKE: 110 | logging.critical(f"You are running fake with {mode} mode") 111 | sys.exit(1) 112 | 113 | if mode == Mode.PAST: 114 | from tgcf.past import forward_job # pylint: disable=import-outside-toplevel 115 | 116 | asyncio.run(forward_job()) 117 | else: 118 | from tgcf.live import start_sync # pylint: disable=import-outside-toplevel 119 | 120 | asyncio.run(start_sync()) 121 | 122 | 123 | # AAHNIK 2021 124 | -------------------------------------------------------------------------------- /tgcf/past.py: -------------------------------------------------------------------------------- 1 | """The module for running tgcf in past mode. 2 | 3 | - past mode can only operate with a user account. 4 | - past mode deals with all existing messages. 5 | """ 6 | 7 | import asyncio 8 | import logging 9 | import time 10 | 11 | from telethon import TelegramClient 12 | from telethon.errors.rpcerrorlist import FloodWaitError 13 | from telethon.tl.custom.message import Message 14 | from telethon.tl.patched import MessageService 15 | 16 | from tgcf import config 17 | from tgcf import storage as st 18 | from tgcf.config import CONFIG, get_SESSION, write_config 19 | from tgcf.plugins import apply_plugins, load_async_plugins 20 | from tgcf.utils import clean_session_files, send_message 21 | 22 | 23 | async def forward_job() -> None: 24 | """Forward all existing messages in the concerned chats.""" 25 | clean_session_files() 26 | 27 | # load async plugins defined in plugin_models 28 | await load_async_plugins() 29 | 30 | if CONFIG.login.user_type != 1: 31 | logging.warning( 32 | "You cannot use bot account for tgcf past mode. Telegram does not allow bots to access chat history." 33 | ) 34 | return 35 | SESSION = get_SESSION() 36 | async with TelegramClient( 37 | SESSION, CONFIG.login.API_ID, CONFIG.login.API_HASH 38 | ) as client: 39 | config.from_to = await config.load_from_to(client, config.CONFIG.forwards) 40 | client: TelegramClient 41 | for from_to, forward in zip(config.from_to.items(), config.CONFIG.forwards): 42 | src, dest = from_to 43 | last_id = 0 44 | forward: config.Forward 45 | logging.info(f"Forwarding messages from {src} to {dest}") 46 | async for message in client.iter_messages( 47 | src, reverse=True, offset_id=forward.offset 48 | ): 49 | message: Message 50 | event = st.DummyEvent(message.chat_id, message.id) 51 | event_uid = st.EventUid(event) 52 | 53 | if forward.end and last_id > forward.end: 54 | continue 55 | if isinstance(message, MessageService): 56 | continue 57 | try: 58 | tm = await apply_plugins(message) 59 | if not tm: 60 | continue 61 | st.stored[event_uid] = {} 62 | 63 | if message.is_reply: 64 | r_event = st.DummyEvent( 65 | message.chat_id, message.reply_to_msg_id 66 | ) 67 | r_event_uid = st.EventUid(r_event) 68 | for d in dest: 69 | if message.is_reply and r_event_uid in st.stored: 70 | tm.reply_to = st.stored.get(r_event_uid).get(d) 71 | fwded_msg = await send_message(d, tm) 72 | st.stored[event_uid].update({d: fwded_msg.id}) 73 | tm.clear() 74 | last_id = message.id 75 | logging.info(f"forwarding message with id = {last_id}") 76 | forward.offset = last_id 77 | write_config(CONFIG, persist=False) 78 | time.sleep(CONFIG.past.delay) 79 | logging.info(f"slept for {CONFIG.past.delay} seconds") 80 | 81 | except FloodWaitError as fwe: 82 | logging.info(f"Sleeping for {fwe}") 83 | await asyncio.sleep(delay=fwe.seconds) 84 | except Exception as err: 85 | logging.exception(err) 86 | -------------------------------------------------------------------------------- /tgcf/web_ui/pages/5_🏃_Run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import subprocess 4 | import time 5 | 6 | import streamlit as st 7 | 8 | from tgcf.config import CONFIG, read_config, write_config 9 | from tgcf.web_ui.password import check_password 10 | from tgcf.web_ui.utils import hide_st, switch_theme 11 | 12 | CONFIG = read_config() 13 | 14 | 15 | def termination(): 16 | st.code("process terminated!") 17 | os.rename("logs.txt", "old_logs.txt") 18 | with open("old_logs.txt", "r") as f: 19 | st.download_button( 20 | "Download last logs", data=f.read(), file_name="tgcf_logs.txt" 21 | ) 22 | 23 | CONFIG = read_config() 24 | CONFIG.pid = 0 25 | write_config(CONFIG) 26 | st.button("Refresh page") 27 | 28 | 29 | st.set_page_config( 30 | page_title="Run", 31 | page_icon="🏃", 32 | ) 33 | hide_st(st) 34 | switch_theme(st,CONFIG) 35 | if check_password(st): 36 | with st.expander("Configure Run"): 37 | CONFIG.show_forwarded_from = st.checkbox( 38 | "Show 'Forwarded from'", value=CONFIG.show_forwarded_from 39 | ) 40 | mode = st.radio("Choose mode", ["live", "past"], index=CONFIG.mode) 41 | if mode == "past": 42 | CONFIG.mode = 1 43 | st.warning( 44 | "Only User Account can be used in Past mode. Telegram does not allow bot account to go through history of a chat!" 45 | ) 46 | CONFIG.past.delay = st.slider( 47 | "Delay in seconds", 0, 100, value=CONFIG.past.delay 48 | ) 49 | else: 50 | CONFIG.mode = 0 51 | CONFIG.live.delete_sync = st.checkbox( 52 | "Sync when a message is deleted", value=CONFIG.live.delete_sync 53 | ) 54 | 55 | if st.button("Save"): 56 | write_config(CONFIG) 57 | 58 | check = False 59 | 60 | if CONFIG.pid == 0: 61 | check = st.button("Run", type="primary") 62 | 63 | if CONFIG.pid != 0: 64 | st.warning( 65 | "You must click stop and then re-run tgcf to apply changes in config." 66 | ) 67 | # check if process is running using pid 68 | try: 69 | os.kill(CONFIG.pid, signal.SIGCONT) 70 | except Exception as err: 71 | st.code("The process has stopped.") 72 | st.code(err) 73 | CONFIG.pid = 0 74 | write_config(CONFIG) 75 | time.sleep(1) 76 | st.experimental_rerun() 77 | 78 | stop = st.button("Stop", type="primary") 79 | if stop: 80 | try: 81 | os.kill(CONFIG.pid, signal.SIGSTOP) 82 | except Exception as err: 83 | st.code(err) 84 | 85 | CONFIG.pid = 0 86 | write_config(CONFIG) 87 | st.button("Refresh Page") 88 | 89 | else: 90 | termination() 91 | 92 | if check: 93 | with open("logs.txt", "w") as logs: 94 | process = subprocess.Popen( 95 | ["tgcf", "--loud", mode], 96 | stdout=logs, 97 | stderr=subprocess.STDOUT, 98 | ) 99 | CONFIG.pid = process.pid 100 | write_config(CONFIG) 101 | time.sleep(2) 102 | 103 | st.experimental_rerun() 104 | 105 | try: 106 | lines = st.slider( 107 | "Lines of logs to show", min_value=100, max_value=1000, step=100 108 | ) 109 | temp_logs = "logs_n_lines.txt" 110 | os.system(f"rm {temp_logs}") 111 | with open("logs.txt", "r") as file: 112 | pass 113 | 114 | os.system(f"tail -n {lines} logs.txt >> {temp_logs}") 115 | with open(temp_logs, "r") as file: 116 | st.code(file.read()) 117 | except FileNotFoundError as err: 118 | st.write("No present logs found") 119 | st.button("Load more logs") 120 | -------------------------------------------------------------------------------- /tgcf/web_ui/pages/3_🔗_Connections.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import streamlit as st 4 | 5 | from tgcf.config import CONFIG, Forward, read_config, write_config 6 | from tgcf.web_ui.password import check_password 7 | from tgcf.web_ui.utils import get_list, get_string, hide_st, switch_theme 8 | 9 | CONFIG = read_config() 10 | 11 | st.set_page_config( 12 | page_title="Connections", 13 | page_icon="🔗", 14 | ) 15 | hide_st(st) 16 | switch_theme(st,CONFIG) 17 | if check_password(st): 18 | add_new = st.button("Add new connection") 19 | if add_new: 20 | CONFIG.forwards.append(Forward()) 21 | write_config(CONFIG) 22 | 23 | num = len(CONFIG.forwards) 24 | 25 | if num == 0: 26 | st.write( 27 | "No connections found. Click on Add new connection above to create one!" 28 | ) 29 | else: 30 | tab_strings = [] 31 | for i in range(num): 32 | if CONFIG.forwards[i].con_name: 33 | label = CONFIG.forwards[i].con_name 34 | else: 35 | label = f"Connection {i+1}" 36 | if CONFIG.forwards[i].use_this: 37 | status = "🟢" 38 | else: 39 | status = "🟡" 40 | 41 | tab_strings.append(f"{status} {label}") 42 | 43 | tabs = st.tabs(list(tab_strings)) 44 | 45 | for i in range(num): 46 | with tabs[i]: 47 | con = i + 1 48 | name = CONFIG.forwards[i].con_name 49 | if name: 50 | label = f"{con} [{name}]" 51 | else: 52 | label = con 53 | with st.expander("Modify Metadata"): 54 | st.write(f"Connection ID: **{con}**") 55 | CONFIG.forwards[i].con_name = st.text_input( 56 | "Name of this connection", 57 | value=CONFIG.forwards[i].con_name, 58 | key=con, 59 | ) 60 | 61 | st.info( 62 | "You can untick the below checkbox to suspend this connection." 63 | ) 64 | CONFIG.forwards[i].use_this = st.checkbox( 65 | "Use this connection", 66 | value=CONFIG.forwards[i].use_this, 67 | key=f"use {con}", 68 | ) 69 | with st.expander("Source and Destination"): 70 | st.write(f"Configure connection {label}") 71 | 72 | CONFIG.forwards[i].source = st.text_input( 73 | "Source", 74 | value=CONFIG.forwards[i].source, 75 | key=f"source {con}", 76 | ).strip() 77 | st.write("only one source is allowed in a connection") 78 | CONFIG.forwards[i].dest = get_list( 79 | st.text_area( 80 | "Destinations", 81 | value=get_string(CONFIG.forwards[i].dest), 82 | key=f"dest {con}", 83 | ) 84 | ) 85 | st.write("Write destinations one item per line") 86 | 87 | with st.expander("Past Mode Settings"): 88 | CONFIG.forwards[i].offset = int( 89 | st.text_input( 90 | "Offset", 91 | value=str(CONFIG.forwards[i].offset), 92 | key=f"offset {con}", 93 | ) 94 | ) 95 | CONFIG.forwards[i].end = int( 96 | st.text_input( 97 | "End", value=str(CONFIG.forwards[i].end), key=f"end {con}" 98 | ) 99 | ) 100 | with st.expander("Delete this connection"): 101 | st.warning( 102 | f"Clicking the 'Remove' button will **delete** connection **{label}**. This action cannot be reversed once done.", 103 | icon="⚠️", 104 | ) 105 | 106 | if st.button(f"Remove connection **{label}**"): 107 | del CONFIG.forwards[i] 108 | write_config(CONFIG) 109 | st.experimental_rerun() 110 | 111 | if st.button("Save"): 112 | write_config(CONFIG) 113 | st.experimental_rerun() 114 | -------------------------------------------------------------------------------- /tgcf/bot/live_bot.py: -------------------------------------------------------------------------------- 1 | """A bot to controll settings for tgcf live mode.""" 2 | 3 | import logging 4 | 5 | import yaml 6 | from telethon import events 7 | 8 | from tgcf import config, const, plugins 9 | from tgcf.bot.utils import ( 10 | admin_protect, 11 | display_forwards, 12 | get_args, 13 | get_command_prefix, 14 | remove_source, 15 | ) 16 | from tgcf.config import CONFIG, write_config 17 | from tgcf.plugin_models import Style 18 | 19 | 20 | @admin_protect 21 | async def forward_command_handler(event): 22 | """Handle the `/forward` command.""" 23 | notes = """The `/forward` command allows you to add a new forward. 24 | Example: suppose you want to forward from a to (b and c) 25 | 26 | ``` 27 | /forward source: a 28 | dest: [b,c] 29 | ``` 30 | 31 | a,b,c are chat ids 32 | 33 | """.replace( 34 | " ", "" 35 | ) 36 | 37 | try: 38 | args = get_args(event.message.text) 39 | if not args: 40 | raise ValueError(f"{notes}\n{display_forwards(config.CONFIG.forwards)}") 41 | 42 | parsed_args = yaml.safe_load(args) 43 | forward = config.Forward(**parsed_args) 44 | try: 45 | remove_source(forward.source, config.CONFIG.forwards) 46 | except: 47 | pass 48 | CONFIG.forwards.append(forward) 49 | config.from_to = await config.load_from_to(event.client, config.CONFIG.forwards) 50 | 51 | await event.respond("Success") 52 | write_config(config.CONFIG) 53 | except ValueError as err: 54 | logging.error(err) 55 | await event.respond(str(err)) 56 | 57 | finally: 58 | raise events.StopPropagation 59 | 60 | 61 | @admin_protect 62 | async def remove_command_handler(event): 63 | """Handle the /remove command.""" 64 | notes = """The `/remove` command allows you to remove a source from forwarding. 65 | Example: Suppose you want to remove the channel with id -100, then run 66 | 67 | `/remove source: -100` 68 | 69 | """.replace( 70 | " ", "" 71 | ) 72 | 73 | try: 74 | args = get_args(event.message.text) 75 | if not args: 76 | raise ValueError(f"{notes}\n{display_forwards(config.CONFIG.forwards)}") 77 | 78 | parsed_args = yaml.safe_load(args) 79 | source_to_remove = parsed_args.get("source") 80 | CONFIG.forwards = remove_source(source_to_remove, config.CONFIG.forwards) 81 | config.from_to = await config.load_from_to(event.client, config.CONFIG.forwards) 82 | 83 | await event.respond("Success") 84 | write_config(config.CONFIG) 85 | except ValueError as err: 86 | logging.error(err) 87 | await event.respond(str(err)) 88 | 89 | finally: 90 | raise events.StopPropagation 91 | 92 | 93 | @admin_protect 94 | async def style_command_handler(event): 95 | """Handle the /style command""" 96 | notes = """This command is used to set the style of the messages to be forwarded. 97 | 98 | Example: `/style bold` 99 | 100 | Options are preserve,normal,bold,italics,code, strike 101 | 102 | """.replace( 103 | " ", "" 104 | ) 105 | 106 | try: 107 | args = get_args(event.message.text) 108 | if not args: 109 | raise ValueError(f"{notes}\n") 110 | _valid = [item.value for item in Style] 111 | if args not in _valid: 112 | raise ValueError(f"Invalid style. Choose from {_valid}") 113 | CONFIG.plugins.fmt.style = args 114 | await event.respond("Success") 115 | write_config(CONFIG) 116 | except ValueError as err: 117 | logging.error(err) 118 | await event.respond(str(err)) 119 | 120 | finally: 121 | raise events.StopPropagation 122 | 123 | 124 | async def start_command_handler(event): 125 | """Handle the /start command.""" 126 | await event.respond(CONFIG.bot_messages.start) 127 | 128 | 129 | async def help_command_handler(event): 130 | """Handle the /help command.""" 131 | await event.respond(CONFIG.bot_messages.bot_help) 132 | 133 | 134 | def get_events(): 135 | _ = get_command_prefix() 136 | logging.info(f"Command prefix is . for userbot and / for bot") 137 | command_events = { 138 | "start": (start_command_handler, events.NewMessage(pattern=f"{_}start")), 139 | "forward": (forward_command_handler, events.NewMessage(pattern=f"{_}forward")), 140 | "remove": (remove_command_handler, events.NewMessage(pattern=f"{_}remove")), 141 | "style": (style_command_handler, events.NewMessage(pattern=f"{_}style")), 142 | "help": (help_command_handler, events.NewMessage(pattern=f"{_}help")), 143 | } 144 | 145 | return command_events 146 | -------------------------------------------------------------------------------- /tgcf/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | """Subpackage of tgcf: plugins. 2 | 3 | Contains all the first-party tgcf plugins. 4 | """ 5 | 6 | 7 | import inspect 8 | import logging 9 | from enum import Enum 10 | from importlib import import_module 11 | from typing import Any, Dict 12 | 13 | from telethon.tl.custom.message import Message 14 | 15 | from tgcf.config import CONFIG 16 | from tgcf.plugin_models import FileType, ASYNC_PLUGIN_IDS 17 | from tgcf.utils import cleanup, stamp 18 | 19 | PLUGINS = CONFIG.plugins 20 | 21 | 22 | class TgcfMessage: 23 | def __init__(self, message: Message) -> None: 24 | self.message = message 25 | self.text = self.message.text 26 | self.raw_text = self.message.raw_text 27 | self.sender_id = self.message.sender_id 28 | self.file_type = self.guess_file_type() 29 | self.new_file = None 30 | self.cleanup = False 31 | self.reply_to = None 32 | self.client = self.message.client 33 | 34 | async def get_file(self) -> str: 35 | """Downloads the file in the message and returns the path where its saved.""" 36 | if self.file_type == FileType.NOFILE: 37 | raise FileNotFoundError("No file exists in this message.") 38 | self.file = stamp(await self.message.download_media(""), self.sender_id) 39 | return self.file 40 | 41 | def guess_file_type(self) -> FileType: 42 | for i in FileType: 43 | if i == FileType.NOFILE: 44 | return i 45 | obj = getattr(self.message, i.value) 46 | if obj: 47 | return i 48 | 49 | def clear(self) -> None: 50 | if self.new_file and self.cleanup: 51 | cleanup(self.new_file) 52 | self.new_file = None 53 | 54 | 55 | class TgcfPlugin: 56 | id_ = "plugin" 57 | 58 | def __init__(self, data: Dict[str, Any]) -> None: # TODO data type has changed 59 | self.data = data 60 | 61 | async def __ainit__(self) -> None: 62 | """Asynchronous initialization here.""" 63 | 64 | def modify(self, tm: TgcfMessage) -> TgcfMessage: 65 | """Modify the message here.""" 66 | return tm 67 | 68 | 69 | def load_plugins() -> Dict[str, TgcfPlugin]: 70 | """Load the plugins specified in config.""" 71 | _plugins = {} 72 | for item in PLUGINS: 73 | plugin_id = item[0] 74 | if item[1].check == False: 75 | continue 76 | 77 | plugin_class_name = f"Tgcf{plugin_id.title()}" 78 | 79 | try: # try to load first party plugin 80 | plugin_module = import_module("tgcf.plugins." + plugin_id) 81 | except ModuleNotFoundError: 82 | logging.error( 83 | f"{plugin_id} is not a first party plugin. Third party plugins are not supported." 84 | ) 85 | else: 86 | logging.info(f"First party plugin {plugin_id} loaded!") 87 | 88 | try: 89 | plugin_class = getattr(plugin_module, plugin_class_name) 90 | if not issubclass(plugin_class, TgcfPlugin): 91 | logging.error( 92 | f"Plugin class {plugin_class_name} does not inherit TgcfPlugin" 93 | ) 94 | continue 95 | plugin: TgcfPlugin = plugin_class(item[1]) 96 | if not plugin.id_ == plugin_id: 97 | logging.error(f"Plugin id for {plugin_id} does not match expected id.") 98 | continue 99 | except AttributeError: 100 | logging.error(f"Found plugin {plugin_id}, but plugin class not found.") 101 | else: 102 | logging.info(f"Loaded plugin {plugin_id}") 103 | _plugins.update({plugin.id_: plugin}) 104 | return _plugins 105 | 106 | 107 | async def load_async_plugins() -> None: 108 | """Load async plugins specified plugin_models.""" 109 | if plugins: 110 | for id in ASYNC_PLUGIN_IDS: 111 | if id in plugins: 112 | await plugins[id].__ainit__() 113 | logging.info(f"Plugin {id} asynchronously loaded") 114 | 115 | 116 | async def apply_plugins(message: Message) -> TgcfMessage: 117 | """Apply all loaded plugins to a message.""" 118 | tm = TgcfMessage(message) 119 | 120 | for _id, plugin in plugins.items(): 121 | try: 122 | if inspect.iscoroutinefunction(plugin.modify): 123 | ntm = await plugin.modify(tm) 124 | else: 125 | ntm = plugin.modify(tm) 126 | except Exception as err: 127 | logging.error(f"Failed to apply plugin {_id}. \n {err} ") 128 | else: 129 | logging.info(f"Applied plugin {_id}") 130 | if not ntm: 131 | tm.clear() 132 | return None 133 | return tm 134 | 135 | 136 | plugins = load_plugins() 137 | -------------------------------------------------------------------------------- /tgcf/live.py: -------------------------------------------------------------------------------- 1 | """The module responsible for operating tgcf in live mode.""" 2 | 3 | import logging 4 | import os 5 | import sys 6 | from typing import Union 7 | 8 | from telethon import TelegramClient, events, functions, types 9 | from telethon.sessions import StringSession 10 | from telethon.tl.custom.message import Message 11 | 12 | from tgcf import config, const 13 | from tgcf import storage as st 14 | from tgcf.bot import get_events 15 | from tgcf.config import CONFIG, get_SESSION 16 | from tgcf.plugins import apply_plugins, load_async_plugins 17 | from tgcf.utils import clean_session_files, send_message 18 | 19 | 20 | async def new_message_handler(event: Union[Message, events.NewMessage]) -> None: 21 | """Process new incoming messages.""" 22 | chat_id = event.chat_id 23 | 24 | if chat_id not in config.from_to: 25 | return 26 | logging.info(f"New message received in {chat_id}") 27 | message = event.message 28 | 29 | event_uid = st.EventUid(event) 30 | 31 | length = len(st.stored) 32 | exceeding = length - const.KEEP_LAST_MANY 33 | 34 | if exceeding > 0: 35 | for key in st.stored: 36 | del st.stored[key] 37 | break 38 | 39 | dest = config.from_to.get(chat_id) 40 | 41 | tm = await apply_plugins(message) 42 | if not tm: 43 | return 44 | 45 | if event.is_reply: 46 | r_event = st.DummyEvent(chat_id, event.reply_to_msg_id) 47 | r_event_uid = st.EventUid(r_event) 48 | 49 | st.stored[event_uid] = {} 50 | for d in dest: 51 | if event.is_reply and r_event_uid in st.stored: 52 | tm.reply_to = st.stored.get(r_event_uid).get(d) 53 | fwded_msg = await send_message(d, tm) 54 | st.stored[event_uid].update({d: fwded_msg}) 55 | tm.clear() 56 | 57 | 58 | async def edited_message_handler(event) -> None: 59 | """Handle message edits.""" 60 | message = event.message 61 | 62 | chat_id = event.chat_id 63 | 64 | if chat_id not in config.from_to: 65 | return 66 | 67 | logging.info(f"Message edited in {chat_id}") 68 | 69 | event_uid = st.EventUid(event) 70 | 71 | tm = await apply_plugins(message) 72 | 73 | if not tm: 74 | return 75 | 76 | fwded_msgs = st.stored.get(event_uid) 77 | 78 | if fwded_msgs: 79 | for _, msg in fwded_msgs.items(): 80 | if config.CONFIG.live.delete_on_edit == message.text: 81 | await msg.delete() 82 | await message.delete() 83 | else: 84 | await msg.edit(tm.text) 85 | return 86 | 87 | dest = config.from_to.get(chat_id) 88 | 89 | for d in dest: 90 | await send_message(d, tm) 91 | tm.clear() 92 | 93 | 94 | async def deleted_message_handler(event): 95 | """Handle message deletes.""" 96 | chat_id = event.chat_id 97 | if chat_id not in config.from_to: 98 | return 99 | 100 | logging.info(f"Message deleted in {chat_id}") 101 | 102 | event_uid = st.EventUid(event) 103 | fwded_msgs = st.stored.get(event_uid) 104 | if fwded_msgs: 105 | for _, msg in fwded_msgs.items(): 106 | await msg.delete() 107 | return 108 | 109 | 110 | ALL_EVENTS = { 111 | "new": (new_message_handler, events.NewMessage()), 112 | "edited": (edited_message_handler, events.MessageEdited()), 113 | "deleted": (deleted_message_handler, events.MessageDeleted()), 114 | } 115 | 116 | 117 | async def start_sync() -> None: 118 | """Start tgcf live sync.""" 119 | # clear past session files 120 | clean_session_files() 121 | 122 | # load async plugins defined in plugin_models 123 | await load_async_plugins() 124 | 125 | SESSION = get_SESSION() 126 | client = TelegramClient( 127 | SESSION, 128 | CONFIG.login.API_ID, 129 | CONFIG.login.API_HASH, 130 | sequential_updates=CONFIG.live.sequential_updates, 131 | ) 132 | if CONFIG.login.user_type == 0: 133 | if CONFIG.login.BOT_TOKEN == "": 134 | logging.warning("Bot token not found, but login type is set to bot.") 135 | sys.exit() 136 | await client.start(bot_token=CONFIG.login.BOT_TOKEN) 137 | else: 138 | await client.start() 139 | config.is_bot = await client.is_bot() 140 | logging.info(f"config.is_bot={config.is_bot}") 141 | command_events = get_events() 142 | 143 | await config.load_admins(client) 144 | 145 | ALL_EVENTS.update(command_events) 146 | 147 | for key, val in ALL_EVENTS.items(): 148 | if config.CONFIG.live.delete_sync is False and key == "deleted": 149 | continue 150 | client.add_event_handler(*val) 151 | logging.info(f"Added event handler for {key}") 152 | 153 | if config.is_bot and const.REGISTER_COMMANDS: 154 | await client( 155 | functions.bots.SetBotCommandsRequest( 156 | scope=types.BotCommandScopeDefault(), 157 | lang_code="en", 158 | commands=[ 159 | types.BotCommand(command=key, description=value) 160 | for key, value in const.COMMANDS.items() 161 | ], 162 | ) 163 | ) 164 | config.from_to = await config.load_from_to(client, config.CONFIG.forwards) 165 | await client.run_until_disconnected() 166 | -------------------------------------------------------------------------------- /.github/archives/readme_old.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | tgcf logo 5 |

6 | 7 |

tgcf

8 | 9 |

10 | The ultimate tool to automate custom telegram message forwarding. 11 |

12 | 13 |

14 | GitHub license 15 | GitHub stars 16 | GitHub issues 17 | PyPI 18 | Twitter 19 |

20 |

21 | Code Quality 22 |

23 | 24 | 25 |
26 | 27 | --- 28 | ! warning ! this is the readme for version 0.2 (old version), and many information may not be true for present version. 29 | --- 30 | --- 31 | 32 |
33 | Live-syncer, Auto-poster, backup-bot, cloner, chat-forwarder, duplicator, ... 34 | 35 | Call it whatever you like! tgcf can fulfill your custom needs. 36 | 37 | The *key features* are: 38 | 39 | 1. Forward messages as "forwarded" or 40 | send a copy of the messages from source to destination chats. 41 | 42 | > A chat can be anything: a group, channel, person or even another bot. 43 | 44 | 2. Supports two [modes](https://github.com/aahnik/tgcf/wiki/Past-vs-Live-modes-explained) 45 | of operation _past_ or _live_. 46 | 47 | > The past mode deals with all existing messages, 48 | > while the live mode is for upcoming ones. 49 | 50 | 3. You may [login](https://github.com/aahnik/tgcf/wiki/Login-with-a-bot-or-user-account) 51 | with a _bot_ or an _user_ account. 52 | 53 | > Telegram imposes certain 54 | [limitations](https://github.com/aahnik/tgcf/wiki/Using-bot-accounts#limitations) 55 | on bot accounts. 56 | You may use an user account to perform the forwards if you wish. 57 | 58 | 4. Perform custom manipulation on messages. 59 | 60 | > You can 61 | [filter](https://github.com/aahnik/tgcf/wiki/How-to-use-filters-%3F), 62 | [format](https://github.com/aahnik/tgcf/wiki/Format-text-before-sending-to-destination), 63 | [replace](https://github.com/aahnik/tgcf/wiki/Text-Replacement-feature-explained), 64 | [watermark](https://github.com/aahnik/tgcf/wiki/How-to-use--watermarking-%3F), 65 | [ocr](https://github.com/aahnik/tgcf/wiki/You-can-do-OCR) 66 | and do whatever else you need ! 67 | 68 | 5. Detailed [wiki](https://github.com/aahnik/tgcf/wiki) + 69 | Video tutorial. 70 | > You can also [get help](#getting-help) from the community. 71 | 72 | 6. If you are a python developer, writing 73 | [plugins](https://github.com/aahnik/tgcf/wiki/How-to-write-a-plugin-for-tgcf-%3F) 74 | for tgcf is like stealing candy from a baby. 75 | > Plugins modify the message before they are sent to the destination chat. 76 | 77 | What are you waiting for? Star the repo and click Watch to recieve updates. 78 | 79 | 80 | ## Video Tutorial 81 | 82 | A youtube video is coming soon. [Subscribe](https://www.youtube.com/channel/UCcEbN0d8iLTB6ZWBE_IDugg) to get notified. 83 | 84 | 85 | 86 | ## Installation 87 | 88 | - If you are an **Windows** user, who is not familiar with the command line, the 89 | [Windows guide](https://github.com/aahnik/tgcf/wiki/Run-tgcf-on-Windows) 90 | is for you. 91 | 92 | - To install tgcf on **Android** (Termux), there exists an installer script, 93 | that allows you to install all dependencies by running just a single line command. 94 | Read the 95 | [guide for android](https://github.com/aahnik/tgcf/wiki/Run-on-Android-using-Termux) 96 | to learn. 97 | 98 | - If you are familiar with **Docker**, you may read the 99 | [docker guide](https://github.com/aahnik/tgcf/wiki/Install-and-run-using-docker) 100 | for an isolated installation. 101 | 102 | - Otherwise for **Linux/Mac**, 103 | you may install `tgcf` via python's package manager `pip`. 104 | 105 | > **Note:** Make sure you have Python 3.8 or above installed. 106 | Go to [python.org](https://python.org) to download python. 107 | 108 | Open your terminal and run the following commands. 109 | 110 | ```shell 111 | pip install --upgrade tgcf 112 | ``` 113 | 114 | To check if the installation succeeded, run 115 | 116 | ```shell 117 | tgcf --version 118 | ``` 119 | 120 | ## Usage 121 | 122 | Configuring `tgcf` is easy. You just need two files in your present directory 123 | (from which tgcf is invoked). 124 | 125 | - [`.env`](https://github.com/aahnik/tgcf/wiki/Environment-Variables) 126 | : To define your environment variables easily. 127 | 128 | - [`tgcf.config.yml`](https://github.com/aahnik/tgcf/wiki/How-to-configure-tgcf-%3F) 129 | : An `yaml` file to configure how `tgcf` behaves. 130 | 131 | In your terminal, just run `tgcf live` or `tgcf past` to start `tgcf`. 132 | It will prompt you to enter your phone no. or bot token, when you run it 133 | for the first time. 134 | 135 | For more details run `tgcf --help` or [read wiki](https://github.com/aahnik/tgcf/wiki/CLI-Usage). 136 | 137 | ## Deploy to Cloud 138 | 139 | Click on [this link](https://m.do.co/c/98b725055148) and get **free 100$** 140 | on Digital Ocean. 141 | 142 | [![DigitalOcean Referral Badge](https://web-platforms.sfo2.digitaloceanspaces.com/WWW/Badge%203.svg)](https://www.digitalocean.com/?refcode=98b725055148&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) 143 | 144 | > **NOTE** You will get nothing if you directly sign up from Digital Ocean Home Page. 145 | > **Use the link** above, or **click on the big fat button** above to get free 100$. 146 | 147 | Deploying to a cloud server is an easier alternative if you cannot install 148 | on your own machine. 149 | Cloud servers are very reliable and great for running `tgcf` in live mode 150 | for a long time. 151 | 152 | You can enjoy smooth one-click deploys to the major cloud providers. 153 | 154 | - [Heroku](https://github.com/aahnik/tgcf/wiki/Deploy-to-Heroku) 155 | - [Digital Ocean](https://github.com/aahnik/tgcf/wiki/Deploy-to-Digital-Ocean) 156 | - [Gitpod](https://github.com/aahnik/tgcf/wiki/Run-for-free-on-Gitpod") 157 | - [Python Anywhere](https://github.com/aahnik/tgcf/wiki/Run-on-PythonAnywhere) 158 | - [Google Cloud Run](https://github.com/aahnik/tgcf/wiki/Run-on-Google-Cloud) 159 | - [GitHub Actions](https://github.com/aahnik/tgcf/wiki/Run-tgcf-in-past-mode-periodically) 160 | 161 | ## Getting Help 162 | 163 | - First of all [read the wiki](https://github.com/aahnik/tgcf/wiki) 164 | and [watch the videos](https://www.youtube.com/channel/UCcEbN0d8iLTB6ZWBE_IDugg) 165 | to get started. 166 | 167 | - Type your question in GitHub's Search bar on the top left of this page, 168 | and click "In this repository". 169 | Go through the issues, discussions and wiki pages that appear in the result. 170 | Try re-wording your query a few times before you give up. 171 | 172 | - If your question does not already exist, 173 | feel free to ask your questions in the 174 | [Discussion forum](https://github.com/aahnik/tgcf/discussions/new). 175 | Please avoid duplicates. 176 | 177 | - For reporting bugs or requesting a new feature please use the [issue tracker](https://github.com/aahnik/tgcf/issues/new) 178 | of the repo. 179 | 180 | ## Contributing 181 | 182 | PRs are most welcome! Read the [contributing guidelines](/.github/CONTRIBUTING.md) 183 | to get started. 184 | 185 | If you are not a developer, you may also contribute financially to 186 | incentivise the development of any custom feature you need. 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | tgcf logo 5 |

6 | 7 |

tgcf

8 | 9 |

10 | The ultimate tool to automate custom telegram message forwarding. 11 |

12 | 13 |

14 | GitHub license 15 | GitHub stars 16 | GitHub issues 17 | PyPI 18 | Twitter 19 |

20 |

21 | Code Quality 22 |

23 | 24 | Live-syncer, Auto-poster, backup-bot, cloner, chat-forwarder, duplicator, ... Call it whatever you like! **tgcf** is an advanced telegram chat forwarding automation tool that can fulfill all your custom needs. 25 | 26 | 27 | ## Features 28 | 29 | Extremely easy to get started yet ready for any complex task you throw at it. 30 | 31 | - At its simple form, its just a **telegram message forwarder** that forwards your messages from source to destination chats. 32 | - You can choose the mode: **past** for forward all old(existing messages) or **live** for start forwarding from now. You can either use a telegram bot account or an user account. 33 |
34 | 35 | ![image](https://user-images.githubusercontent.com/66209958/209553073-c6ed1b78-ab8c-43d0-b20d-cd30e543bc34.png) 36 | 37 | - You can cutomize every detail of the forwarding with the help of plugins: **filter**(blacklist/whitelist), **format**(bold, italics, etc), **replace**(supports regex), **caption**(header/footer). You can even apply watermark to images/videos, or perform optical character recognition (ocr) on images. 38 |
39 | 40 | ![image](https://user-images.githubusercontent.com/66209958/209553374-8a6f9a5a-8095-4ca7-9f7f-acafe61d9932.png) 41 | 42 | - tgcf comes with a **web interface** to customize all these options. You may define you **config in json**, and **run tgcf from the CLI** if you wish. 43 | 44 | ![image](https://user-images.githubusercontent.com/66209958/209554118-c657e361-8ce2-462d-a305-04e44754cbf7.png) 45 | ![image](https://user-images.githubusercontent.com/66209958/209554345-1db31eff-7694-47ef-aede-6a77a7cefb83.png) 46 | 47 | 48 | 49 | - Detailed [**documentation**](https://github.com/aahnik/tgcf/wiki) and [**videos**](https://www.youtube.com/playlist?list=PLSTrsq_DvEgisMG5BLUf97tp2DoAnwCMG) makes it easy for you to configure tgcf and deploy to any platform of your choice. 50 | The following videos (english) explain everything in great detail. 51 | - [Feature Overview](https://youtu.be/FclVGY-K70M) 52 | - [Running on Windows/Mac/Linux](https://youtu.be/5GzHb6J7mc0) 53 | 54 | - [Deploy to Digital Ocean Droplet](https://youtu.be/0p0JkJpfTA0) 55 | - Supported environments **Linux**, **Mac**, Windows (Running Ubuntu on top of **WSL-2**), **Android** (Using Termux app) and any platform where running **Docker** containers is supported. 56 | - All these is **free and open source**, with not a single feature behind a paywall. Tgcf serves to be a free alternative to many commercial telegram bots out there. However you may sponsor to accelerate the development of any new feature and get fast support over chat. 57 | 58 | 59 | ## Install and Run 60 | 61 | If you want to use tgcf for free, then run on your own desktop or mobile computer. 62 | 63 | Make sure you are on a supported environment and have python:3.10 or above, installed. 64 | 65 | - Create a directory and move into it. 66 | 67 | ```shell 68 | mkdir my-tgcf 69 | cd my-tgcf 70 | ``` 71 | 72 | - Create a python virtual environment and activate it. 73 | 74 | ```shell 75 | python3 -m venv .venv 76 | source .venv/bin/activate 77 | ``` 78 | 79 | - Install tgcf using `pip` 80 | 81 | ```shell 82 | pip install tgcf 83 | tgcf --version 84 | ``` 85 | 86 | - Set the password for accessing web interface. 87 | The password is to be set in the `.env` file. 88 | 89 | ```shell 90 | echo "PASSWORD=hocus pocus qwerty utopia" >> .env 91 | ``` 92 | 93 | Set your own password, instead of whats given above. 94 | 95 | _Security advice_: 96 | 97 | - Please make sure the password has more than 16 characters. 98 | - You can save your password in any password manager (may be of browser) 99 | to autofill password everytime. 100 | 101 | - Start the web-server. 102 | 103 | ```shell 104 | tgcf-web 105 | ``` 106 | 107 | To run tgcf without the web-ui read about 108 | [tgcf cli](https://github.com/aahnik/tgcf/wiki/CLI-Usage). 109 | 110 | If you are planning to use watermarking and ocr features within tgcf, 111 | you need to install `ffmpeg` and `tesseract-ocr` libraries in you system. 112 | [Read more](https://github.com/aahnik/tgcf/wiki/Additional-Requirements). 113 | 114 | See also: [How to install and run using docker ?](https://github.com/aahnik/tgcf/wiki/Install-and-run-using-docker) 115 | 116 | ## Deploy to Cloud 117 | 118 | Click on [this link](https://m.do.co/c/98b725055148) and get **free 200$** 119 | on Digital Ocean. 120 | 121 | [![DigitalOcean Referral Badge](https://web-platforms.sfo2.digitaloceanspaces.com/WWW/Badge%203.svg)](https://www.digitalocean.com/?refcode=98b725055148&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) 122 | 123 | > **NOTE** You will get nothing if you directly sign up from Digital Ocean Home Page. 124 | > **Use the link** above, or **click on the big fat button** above to get free 200$. 125 | 126 | Deploying to a cloud server is an easier alternative if you cannot install 127 | on your own machine. 128 | Cloud servers are very reliable and great for running `tgcf` in live mode 129 | for a long time. 130 | 131 | Here are some guides for deployment to different cloud providers. 132 | 133 | - [Heroku](https://github.com/aahnik/tgcf/wiki/Deploy-to-Heroku) 134 | - [Digital Ocean](https://github.com/aahnik/tgcf/wiki/Deploy-to-Digital-Ocean) 135 | - [Gitpod](https://github.com/aahnik/tgcf/wiki/Run-for-free-on-Gitpod") 136 | - [Python Anywhere](https://github.com/aahnik/tgcf/wiki/Run-on-PythonAnywhere) 137 | - [Google Cloud Run](https://github.com/aahnik/tgcf/wiki/Run-on-Google-Cloud) 138 | 139 | ## Getting Help 140 | 141 | - First of all [read the wiki](https://github.com/aahnik/tgcf/wiki) 142 | and [watch the videos](https://www.youtube.com/channel/UCcEbN0d8iLTB6ZWBE_IDugg) 143 | to get started. 144 | 145 | - Type your question in GitHub's Search bar on the top left of this page, 146 | and click "In this repository". 147 | Go through the issues, discussions and wiki pages that appear in the result. 148 | Try re-wording your query a few times before you give up. 149 | 150 | - If your question does not already exist, 151 | feel free to ask your questions in the 152 | [Discussion forum](https://github.com/aahnik/tgcf/discussions/new). 153 | Please avoid duplicates. 154 | 155 | - For reporting bugs or requesting a new feature please use the [issue tracker](https://github.com/aahnik/tgcf/issues/new) 156 | of the repo. 157 | 158 | ## Contributing 159 | 160 | PRs are most welcome! Read the [contributing guidelines](/.github/CONTRIBUTING.md) 161 | to get started. 162 | 163 | If you are not a developer, you may also contribute financially to 164 | incentivise the development of any custom feature you need. 165 | -------------------------------------------------------------------------------- /tgcf/config.py: -------------------------------------------------------------------------------- 1 | """Load all user defined config and env vars.""" 2 | 3 | import logging 4 | import os 5 | import sys 6 | from typing import Dict, List, Optional, Union, Any 7 | 8 | from dotenv import load_dotenv 9 | from pydantic import BaseModel, validator # pylint: disable=no-name-in-module 10 | from pymongo import MongoClient 11 | from telethon import TelegramClient 12 | from telethon.sessions import StringSession 13 | 14 | from tgcf import storage as stg 15 | from tgcf.const import CONFIG_FILE_NAME 16 | from tgcf.plugin_models import PluginConfig 17 | 18 | pwd = os.getcwd() 19 | env_file = os.path.join(pwd, ".env") 20 | 21 | load_dotenv(env_file) 22 | 23 | 24 | class Forward(BaseModel): 25 | """Blueprint for the forward object.""" 26 | 27 | # pylint: disable=too-few-public-methods 28 | con_name: str = "" 29 | use_this: bool = True 30 | source: Union[int, str] = "" 31 | dest: List[Union[int, str]] = [] 32 | offset: int = 0 33 | end: Optional[int] = 0 34 | 35 | 36 | class LiveSettings(BaseModel): 37 | """Settings to configure how tgcf operates in live mode.""" 38 | 39 | # pylint: disable=too-few-public-methods 40 | sequential_updates: bool = False 41 | delete_sync: bool = False 42 | delete_on_edit: Optional[str] = ".deleteMe" 43 | 44 | 45 | class PastSettings(BaseModel): 46 | """Configuration for past mode.""" 47 | 48 | # pylint: disable=too-few-public-methods 49 | delay: int = 0 50 | 51 | @validator("delay") 52 | def validate_delay(cls, val): # pylint: disable=no-self-use,no-self-argument 53 | """Check if the delay used by user is values. If not, use closest logical values.""" 54 | if val not in range(0, 101): 55 | logging.warning("delay must be within 0 to 100 seconds") 56 | if val > 100: 57 | val = 100 58 | if val < 0: 59 | val = 0 60 | return val 61 | 62 | 63 | class LoginConfig(BaseModel): 64 | 65 | API_ID: int = 0 66 | API_HASH: str = "" 67 | user_type: int = 0 # 0:bot, 1:user 68 | phone_no: int = 91 69 | USERNAME: str = "" 70 | SESSION_STRING: str = "" 71 | BOT_TOKEN: str = "" 72 | 73 | 74 | class BotMessages(BaseModel): 75 | start: str = "Hi! I am alive" 76 | bot_help: str = "For details visit github.com/aahnik/tgcf" 77 | 78 | 79 | class Config(BaseModel): 80 | """The blueprint for tgcf's whole config.""" 81 | 82 | # pylint: disable=too-few-public- 83 | pid: int = 0 84 | theme: str = "light" 85 | login: LoginConfig = LoginConfig() 86 | admins: List[Union[int, str]] = [] 87 | forwards: List[Forward] = [] 88 | show_forwarded_from: bool = False 89 | mode: int = 0 # 0: live, 1:past 90 | live: LiveSettings = LiveSettings() 91 | past: PastSettings = PastSettings() 92 | 93 | plugins: PluginConfig = PluginConfig() 94 | bot_messages = BotMessages() 95 | 96 | 97 | def write_config_to_file(config: Config): 98 | with open(CONFIG_FILE_NAME, "w", encoding="utf8") as file: 99 | file.write(config.json()) 100 | 101 | 102 | def detect_config_type() -> int: 103 | if os.getenv("MONGO_CON_STR"): 104 | if MONGO_CON_STR: 105 | logging.info("Using mongo db for storing config!") 106 | client = MongoClient(MONGO_CON_STR) 107 | stg.mycol = setup_mongo(client) 108 | return 2 109 | if CONFIG_FILE_NAME in os.listdir(): 110 | logging.info(f"{CONFIG_FILE_NAME} detected!") 111 | return 1 112 | 113 | else: 114 | logging.info( 115 | "config file not found. mongo not found. creating local config file." 116 | ) 117 | cfg = Config() 118 | write_config_to_file(cfg) 119 | logging.info(f"{CONFIG_FILE_NAME} created!") 120 | return 1 121 | 122 | 123 | def read_config(count=1) -> Config: 124 | """Load the configuration defined by user.""" 125 | if count > 3: 126 | logging.warning("Failed to read config, returning default config") 127 | return Config() 128 | if count != 1: 129 | logging.info(f"Trying to read config time:{count}") 130 | try: 131 | if stg.CONFIG_TYPE == 1: 132 | with open(CONFIG_FILE_NAME, encoding="utf8") as file: 133 | return Config.parse_raw(file.read()) 134 | elif stg.CONFIG_TYPE == 2: 135 | return read_db() 136 | else: 137 | return Config() 138 | except Exception as err: 139 | logging.warning(err) 140 | stg.CONFIG_TYPE = detect_config_type() 141 | return read_config(count=count + 1) 142 | 143 | 144 | def write_config(config: Config, persist=True): 145 | """Write changes in config back to file.""" 146 | if stg.CONFIG_TYPE == 1 or stg.CONFIG_TYPE == 0: 147 | write_config_to_file(config) 148 | elif stg.CONFIG_TYPE == 2: 149 | if persist: 150 | update_db(config) 151 | 152 | 153 | def get_env_var(name: str, optional: bool = False) -> str: 154 | """Fetch an env var.""" 155 | var = os.getenv(name, "") 156 | 157 | while not var: 158 | if optional: 159 | return "" 160 | var = input(f"Enter {name}: ") 161 | return var 162 | 163 | 164 | async def get_id(client: TelegramClient, peer): 165 | return await client.get_peer_id(peer) 166 | 167 | 168 | async def load_from_to( 169 | client: TelegramClient, forwards: List[Forward] 170 | ) -> Dict[int, List[int]]: 171 | """Convert a list of Forward objects to a mapping. 172 | 173 | Args: 174 | client: Instance of Telegram client (logged in) 175 | forwards: List of Forward objects 176 | 177 | Returns: 178 | Dict: key = chat id of source 179 | value = List of chat ids of destinations 180 | 181 | Notes: 182 | -> The Forward objects may contain username/phn no/links 183 | -> But this mapping strictly contains signed integer chat ids 184 | -> Chat ids are essential for how storage is implemented 185 | -> Storage is essential for edit, delete and reply syncs 186 | """ 187 | from_to_dict = {} 188 | 189 | async def _(peer): 190 | return await get_id(client, peer) 191 | 192 | for forward in forwards: 193 | if not forward.use_this: 194 | continue 195 | source = forward.source 196 | if not isinstance(source, int) and source.strip() == "": 197 | continue 198 | src = await _(forward.source) 199 | from_to_dict[src] = [await _(dest) for dest in forward.dest] 200 | logging.info(f"From to dict is {from_to_dict}") 201 | return from_to_dict 202 | 203 | 204 | async def load_admins(client: TelegramClient): 205 | for admin in CONFIG.admins: 206 | ADMINS.append(await get_id(client, admin)) 207 | logging.info(f"Loaded admins are {ADMINS}") 208 | return ADMINS 209 | 210 | 211 | def setup_mongo(client): 212 | 213 | mydb = client[MONGO_DB_NAME] 214 | mycol = mydb[MONGO_COL_NAME] 215 | if not mycol.find_one({"_id": 0}): 216 | mycol.insert_one({"_id": 0, "author": "tgcf", "config": Config().dict()}) 217 | 218 | return mycol 219 | 220 | 221 | def update_db(cfg): 222 | stg.mycol.update_one({"_id": 0}, {"$set": {"config": cfg.dict()}}) 223 | 224 | 225 | def read_db(): 226 | obj = stg.mycol.find_one({"_id": 0}) 227 | cfg = Config(**obj["config"]) 228 | return cfg 229 | 230 | 231 | PASSWORD = os.getenv("PASSWORD", "tgcf") 232 | ADMINS = [] 233 | 234 | MONGO_CON_STR = os.getenv("MONGO_CON_STR") 235 | MONGO_DB_NAME = os.getenv("MONGO_DB_NAME", "tgcf-config") 236 | MONGO_COL_NAME = os.getenv("MONGO_COL_NAME", "tgcf-instance-0") 237 | 238 | stg.CONFIG_TYPE = detect_config_type() 239 | CONFIG = read_config() 240 | 241 | if PASSWORD == "tgcf": 242 | logging.warn( 243 | "You have not set a password to protect the web access to tgcf.\nThe default password `tgcf` is used." 244 | ) 245 | from_to = {} 246 | is_bot: Optional[bool] = None 247 | logging.info("config.py got executed") 248 | 249 | 250 | def get_SESSION(section: Any = CONFIG.login, default: str = 'tgcf_bot'): 251 | if section.SESSION_STRING and section.user_type == 1: 252 | logging.info("using session string") 253 | SESSION = StringSession(section.SESSION_STRING) 254 | elif section.BOT_TOKEN and section.user_type == 0: 255 | logging.info("using bot account") 256 | SESSION = default 257 | else: 258 | logging.warning("Login information not set!") 259 | sys.exit() 260 | return SESSION -------------------------------------------------------------------------------- /tgcf/web_ui/pages/4_🔌_Plugins.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import streamlit as st 4 | import yaml 5 | 6 | from tgcf.config import CONFIG, read_config, write_config 7 | from tgcf.plugin_models import FileType, Replace, Style 8 | from tgcf.web_ui.password import check_password 9 | from tgcf.web_ui.utils import get_list, get_string, hide_st, switch_theme 10 | 11 | CONFIG = read_config() 12 | 13 | st.set_page_config( 14 | page_title="Plugins", 15 | page_icon="🔌", 16 | ) 17 | 18 | hide_st(st) 19 | switch_theme(st,CONFIG) 20 | if check_password(st): 21 | 22 | with st.expander("Filter"): 23 | CONFIG.plugins.filter.check = st.checkbox( 24 | "Use this plugin: filter", value=CONFIG.plugins.filter.check 25 | ) 26 | st.write("Blacklist or whitelist certain text items.") 27 | text_tab, users_tab, files_tab = st.tabs(["Text", "Users", "Files"]) 28 | 29 | with text_tab: 30 | CONFIG.plugins.filter.text.case_sensitive = st.checkbox( 31 | "Case Sensitive", value=CONFIG.plugins.filter.text.case_sensitive 32 | ) 33 | CONFIG.plugins.filter.text.regex = st.checkbox( 34 | "Interpret filters as regex", value=CONFIG.plugins.filter.text.regex 35 | ) 36 | 37 | st.write("Enter one text expression per line") 38 | CONFIG.plugins.filter.text.whitelist = get_list( 39 | st.text_area( 40 | "Text Whitelist", 41 | value=get_string(CONFIG.plugins.filter.text.whitelist), 42 | ) 43 | ) 44 | CONFIG.plugins.filter.text.blacklist = get_list( 45 | st.text_area( 46 | "Text Blacklist", 47 | value=get_string(CONFIG.plugins.filter.text.blacklist), 48 | ) 49 | ) 50 | 51 | with users_tab: 52 | st.write("Enter one username/id per line") 53 | CONFIG.plugins.filter.users.whitelist = get_list( 54 | st.text_area( 55 | "Users Whitelist", 56 | value=get_string(CONFIG.plugins.filter.users.whitelist), 57 | ) 58 | ) 59 | CONFIG.plugins.filter.users.blacklist = get_list( 60 | st.text_area( 61 | "Users Blacklist", get_string(CONFIG.plugins.filter.users.blacklist) 62 | ) 63 | ) 64 | 65 | flist = [item.value for item in FileType] 66 | with files_tab: 67 | CONFIG.plugins.filter.files.whitelist = st.multiselect( 68 | "Files Whitelist", flist, default=CONFIG.plugins.filter.files.whitelist 69 | ) 70 | CONFIG.plugins.filter.files.blacklist = st.multiselect( 71 | "Files Blacklist", flist, default=CONFIG.plugins.filter.files.blacklist 72 | ) 73 | 74 | with st.expander("Format"): 75 | CONFIG.plugins.fmt.check = st.checkbox( 76 | "Use this plugin: format", value=CONFIG.plugins.fmt.check 77 | ) 78 | st.write( 79 | "Add style to text like **bold**, _italics_, ~~strikethrough~~, `monospace` etc." 80 | ) 81 | style_list = [item.value for item in Style] 82 | CONFIG.plugins.fmt.style = st.selectbox( 83 | "Format", style_list, index=style_list.index(CONFIG.plugins.fmt.style) 84 | ) 85 | 86 | with st.expander("Watermark"): 87 | if os.system("ffmpeg -version >> /dev/null 2>&1") != 0: 88 | st.warning( 89 | "Could not find `ffmpeg`. Make sure to have `ffmpeg` installed in server to use this plugin." 90 | ) 91 | CONFIG.plugins.mark.check = st.checkbox( 92 | "Apply watermark to media (images and videos).", 93 | value=CONFIG.plugins.mark.check, 94 | ) 95 | uploaded_file = st.file_uploader("Upload watermark image(png)", type=["png"]) 96 | if uploaded_file is not None: 97 | with open("image.png", "wb") as f: 98 | f.write(uploaded_file.getbuffer()) 99 | 100 | with st.expander("OCR"): 101 | st.write("Optical Character Recognition.") 102 | if os.system("tesseract --version >> /dev/null 2>&1") != 0: 103 | st.warning( 104 | "Could not find `tesseract`. Make sure to have `tesseract` installed in server to use this plugin." 105 | ) 106 | CONFIG.plugins.ocr.check = st.checkbox( 107 | "Activate OCR for images", value=CONFIG.plugins.ocr.check 108 | ) 109 | st.write("The text will be added in desciption of image while forwarding.") 110 | 111 | with st.expander("Replace"): 112 | CONFIG.plugins.replace.check = st.checkbox( 113 | "Apply text replacement", value=CONFIG.plugins.replace.check 114 | ) 115 | CONFIG.plugins.replace.regex = st.checkbox( 116 | "Interpret as regex", value=CONFIG.plugins.replace.regex 117 | ) 118 | 119 | CONFIG.plugins.replace.text_raw = st.text_area( 120 | "Replacements", value=CONFIG.plugins.replace.text_raw 121 | ) 122 | try: 123 | replace_dict = yaml.safe_load( 124 | CONFIG.plugins.replace.text_raw 125 | ) # validate and load yaml 126 | if not replace_dict: 127 | replace_dict = {} 128 | temp = Replace(text=replace_dict) # perform validation by pydantic 129 | del temp 130 | except Exception as err: 131 | st.error(err) 132 | CONFIG.plugins.replace.text = {} 133 | else: 134 | CONFIG.plugins.replace.text = replace_dict 135 | 136 | if st.checkbox("Show rules and usage"): 137 | st.markdown( 138 | """ 139 | Replace one word or expression with another. 140 | 141 | - Write every replacement in a new line. 142 | - The original text then **a colon `:`** and then **a space** and then the new text. 143 | - Its recommended to use **single quotes**. Quotes are must when your string contain spaces or special characters. 144 | - Double quotes wont work if your regex has the character: `\` . 145 | ``` 146 | 'orginal': 'new' 147 | 148 | ``` 149 | - View [docs](https://github.com/aahnik/tgcf/wiki/Replace-Plugin) for advanced usage.""" 150 | ) 151 | 152 | with st.expander("Caption"): 153 | CONFIG.plugins.caption.check = st.checkbox( 154 | "Apply Captions", value=CONFIG.plugins.caption.check 155 | ) 156 | CONFIG.plugins.caption.header = st.text_area( 157 | "Header", value=CONFIG.plugins.caption.header 158 | ) 159 | CONFIG.plugins.caption.footer = st.text_area( 160 | "Footer", value=CONFIG.plugins.caption.footer 161 | ) 162 | st.write( 163 | "You can have blank lines inside header and footer, to make space between the orignal message and captions." 164 | ) 165 | 166 | with st.expander("Sender"): 167 | st.write("Modify the sender of forwarded messages other than the current user/bot") 168 | st.warning("Show 'Forwarded from' option must be disabled or else messages will not be sent",icon="⚠️") 169 | CONFIG.plugins.sender.check = st.checkbox( 170 | "Set sender to:", value=CONFIG.plugins.sender.check 171 | ) 172 | leftpad,content,rightpad = st.columns([0.05,0.9,0.05]) 173 | with content: 174 | user_type = st.radio("Account Type", ["Bot", "User"], index=CONFIG.plugins.sender.user_type,horizontal=True) 175 | if user_type == "Bot": 176 | CONFIG.plugins.sender.user_type = 0 177 | CONFIG.plugins.sender.BOT_TOKEN = st.text_input( 178 | "Bot Token", value=CONFIG.plugins.sender.BOT_TOKEN, type="password" 179 | ) 180 | else: 181 | CONFIG.plugins.sender.user_type = 1 182 | CONFIG.plugins.sender.SESSION_STRING = st.text_input( 183 | "Session String", CONFIG.plugins.sender.SESSION_STRING, type="password" 184 | ) 185 | st.markdown( 186 | """ 187 | ###### How to get session string? 188 | 189 | Link to repl: https://replit.com/@aahnik/tg-login?v=1 190 | 191 |

192 | Click on the above link and enter api id, api hash, and phone no to generate session string. 193 |

194 | 195 | 196 | > **Note from developer:** 197 | > 198 | > Due some issues logging in with a user account using a phone no is not supported in this web interface. 199 | > 200 | > I have built a command-line program named tg-login (https://github.com/aahnik/tg-login) that can generate the session string for you. 201 | > 202 | > You can run tg-login on your computer, or securely in this repl. tg-login is open source, and you can also inspect the bash script running in the repl. 203 | > 204 | > What is a session string? 205 | > https://docs.telethon.dev/en/stable/concepts/sessions.html#string-sessions 206 | """ 207 | ,unsafe_allow_html=True) 208 | 209 | if st.button("Save"): 210 | write_config(CONFIG) 211 | --------------------------------------------------------------------------------