├── .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 |
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 |
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 | 
8 |
9 | Click **Create** -> *Apps*
10 |
11 |
12 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
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 | [](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 | [](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 |
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 | 
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 | 
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 | 
--------------------------------------------------------------------------------
/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 | 
10 |
11 | 2. Install [Powershell]() , [Windows Terminal](), and Python 3.9
12 |
13 |
14 | 
15 |
16 | 
17 |
18 | 
19 |
20 |
21 | 
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 | 
26 |
27 | ## Install tgcf
28 |
29 | Open Powershell in Windows Terminal and run pip install tgcf
30 |
31 | 
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 | 
42 | 
43 | 
44 | 
45 | 
46 |
47 | 3. Open terminal in VS Code and run tgcf
48 |
49 | 
50 | 
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 |
5 |
6 |
7 | tgcf
8 |
9 |
10 | The ultimate tool to automate custom telegram message forwarding.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 | [](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 |
5 |
6 |
7 | tgcf
8 |
9 |
10 | The ultimate tool to automate custom telegram message forwarding.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 | 
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 | 
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 | 
45 | 
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 | [](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 |
--------------------------------------------------------------------------------