├── data └── .gitkeep ├── requirements.txt ├── .gitignore ├── example.env ├── docker-compose.yml ├── .dockerignore ├── Dockerfile ├── LICENSE ├── readme.md └── newsbridge.py /data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | py-cord 2 | telethon 3 | python-dotenv 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # program configs 2 | /*.session 3 | /*.session-journal 4 | .env 5 | 6 | # ide cobfigs 7 | /.idea 8 | /.vscode 9 | 10 | # program data 11 | /data 12 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | # telegram API access 2 | API_ID= 3 | API_HASH='' 4 | 5 | #primary 6 | CHAT_ID_1=-1000000000000 7 | WEBHOOK_URL_1='' 8 | 9 | #secondary 10 | CHAT_ID_2=-1000000000000 11 | WEBHOOK_URL_2='' -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | app: 5 | image: newsbridge 6 | build: 7 | context: . 8 | dockerfile: ./Dockerfile 9 | volumes: 10 | - ./anon.session:/app/anon.session 11 | - ./data:/app/data 12 | env_file: 13 | - .env 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | **/.venv 3 | **/.classpath 4 | **/data 5 | **/.dockerignore 6 | **/.env 7 | **/*.env 8 | **/.git 9 | **/.gitignore 10 | **/.projects 11 | **/.settings 12 | **/*.session 13 | **/.toolstarget 14 | **/.vs 15 | **/.vscode 16 | **/*.*proj.user 17 | **/*.dbmdl 18 | **/*.jfm 19 | **/bin 20 | **/charts 21 | **/docker-compose* 22 | **/compose* 23 | **/Dockerfile* 24 | **/node_modules 25 | **/npm-debug.log 26 | **/obj 27 | **/secrets.dev.yaml 28 | **/values.dev.yaml 29 | 30 | 31 | LICENSE 32 | README.md 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # For more information, please refer to https://aka.ms/vscode-docker-python 2 | FROM python:3.10-slim 3 | 4 | # Keeps Python from generating .pyc files in the container 5 | ENV PYTHONDONTWRITEBYTECODE=1 6 | 7 | # Turns off buffering for easier container logging 8 | ENV PYTHONUNBUFFERED=1 9 | 10 | # Install pip requirements 11 | COPY requirements.txt . 12 | RUN python -m pip install -r requirements.txt 13 | 14 | WORKDIR /app 15 | COPY . /app 16 | 17 | # Creates a non-root user with an explicit UID and adds permission to access the /app folder 18 | # For more info, please refer to https://aka.ms/vscode-docker-python-configure-containers 19 | #RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app 20 | #USER appuser 21 | 22 | # During debugging, this entry point will be overridden. For more information, please refer to https://aka.ms/vscode-docker-python-debug 23 | CMD ["python", "newsbridge.py"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Emhl 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 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Telegram Channel to Discord Newsbridge 2 | 3 | ## What does it do? 4 | 5 | With this program you can bridge/forward all messages from a Telegram Channel through a Discord Webhook onto your server. 6 | 7 | ## Installation 8 | 9 | - clone the project 10 | - enter the directory 11 | - copy the example.env to .env 12 | 13 | ```bash 14 | git clone https://github.com/emhl/newsbridge.git 15 | cd newsbridge 16 | cp example.env .env 17 | ``` 18 | 19 | ### setting up the telegram side 20 | 21 | - either get your Telegram Developer API ID and Hash from [my.telegram.org](https://my.telegram.org/auth) or create a bot with [@BotFather](https://tm.me/BotFather) (the second option only works with public channels or if the bot is in the group/channel and privacy mode is turned off) 22 | - set your API_ID and API_HASH in .env 23 | - get the Telegram Channel ID, you can use this bot [@username_to_id_bot](https://t.me/username_to_id_bot) 24 | - set this value in CHAT_ID_1 25 | 26 | ### Connecting the Webhook 27 | 28 | - create a Discord Webhook [through the server settings](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) 29 | - copy the Webhook Url and set it to WEBHOOK_URL_1 30 | 31 | ### Final Step 32 | 33 | you can either run it with docker or in a python venv (without a virtual environment is strongly discuraged) 34 | 35 | #### Python venv 36 | 37 | Requirements: 38 | - Python 39 | 40 | How to Run 41 | - create venv 42 | - enter the virtual envirenment 43 | - install the dependencies 44 | - run the script 45 | - follow the instructions to authenticate the client. 46 | 47 | ```bash 48 | python -m venv nb-venv 49 | source nb-venv/bin/activate 50 | pip install -r requirements.txt 51 | python newsbridge.py 52 | ``` 53 | 54 | note: too keep it running you could use something like screen, tmux or docker 55 | 56 | #### Docker 57 | 58 | Requrements: 59 | - docker 60 | - docker-compose 61 | 62 | create a session token `anon.session` by running a temporary container in interacrive mode 63 | 64 | ```bash 65 | touch anon.session 66 | docker build -t newsbridge . 67 | docker run -it -v ./anon.session:/app/anon.session \ 68 | -v ./data:/app/data \ 69 | --env-file .env --restart unless-stopped \ 70 | newsbridge:latest 71 | ``` 72 | 73 | after that you can run it with docker-compose 74 | 75 | ```bash 76 | docker-compose up -d 77 | ``` 78 | -------------------------------------------------------------------------------- /newsbridge.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | import aiohttp 5 | import discord 6 | 7 | from telethon import TelegramClient, events 8 | from telethon.tl.types import MessageEntityUrl, MessageEntityTextUrl,\ 9 | MessageEntityBold, MessageEntityItalic, MessageEntityUnderline,\ 10 | MessageEntityStrike, MessageEntityCode 11 | 12 | 13 | load_dotenv() 14 | # Remember to use your own values from my.telegram.org! 15 | API_ID = os.getenv('API_ID') 16 | API_HASH = os.getenv('API_HASH') 17 | tg_client = TelegramClient('anon', API_ID, API_HASH) 18 | 19 | 20 | # bridge 1 21 | CHAT_ID_1 = int(os.getenv('CHAT_ID_1')) 22 | WEBHOOK_URL_1 = os.getenv('WEBHOOK_URL_1') 23 | 24 | 25 | @tg_client.on(events.NewMessage(chats=CHAT_ID_1)) 26 | async def handler1(event): 27 | print('got message on chat 1') 28 | await handler(event.message, WEBHOOK_URL_1) 29 | 30 | # bridge 2 31 | CHAT_ID_2 = int(os.getenv('CHAT_ID_2')) 32 | WEBHOOK_URL_2 = os.getenv('WEBHOOK_URL_2') 33 | 34 | 35 | @tg_client.on(events.NewMessage(chats=CHAT_ID_2)) 36 | async def handler2(event): 37 | print('got message on chat 2') 38 | await handler(event.message, WEBHOOK_URL_2) 39 | 40 | # if you want a third bridge just copy and paste the above code and increment the number 41 | 42 | 43 | async def handler(m, webhook_url): 44 | message = improveMessage(m) 45 | session = aiohttp.ClientSession() 46 | webhook = discord.Webhook.from_url(webhook_url, session=session) 47 | 48 | if (m.file and not m.web_preview): 49 | path = await m.download_media() 50 | print('File saved to', path) # printed after download is done 51 | try: 52 | await webhook.send(file=discord.File(path)) 53 | print('File sent') 54 | except: 55 | print('unable to send file') 56 | os.remove(path) 57 | 58 | if len(message) < 2000: 59 | print('short message') 60 | await webhook.send(message) 61 | print('sent message') 62 | elif len(message) < 3300: 63 | print('long message') 64 | message1 = '' 65 | message2 = '' 66 | lines = message.splitlines() 67 | i = 0 68 | for x in lines: 69 | # print(x) 70 | if i < len(lines)/2: 71 | message1 = message1 + x + '\n' 72 | else: 73 | message2 = message2 + x + '\n' 74 | i = i+1 75 | 76 | await webhook.send(message1) 77 | await webhook.send(message2) 78 | print('sent messages') 79 | else: 80 | print('longer message') 81 | message1 = '' 82 | message2 = '' 83 | message3 = '' 84 | lines = message.splitlines() 85 | i = 0 86 | for x in lines: 87 | # print(x) 88 | if i < len(lines)*0.3: 89 | message1 = message1 + x + '\n' 90 | elif i < len(lines)*0.6: 91 | message2 = message2 + x + '\n' 92 | else: 93 | message3 = message3 + x + '\n' 94 | i = i+1 95 | 96 | await webhook.send(message1) 97 | await webhook.send(message2) 98 | await webhook.send(message3) 99 | print('sent messages') 100 | await session.close() 101 | 102 | 103 | def improveMessage(m): 104 | offset = 0 105 | try: 106 | print(m) 107 | if m.entities: 108 | for entity in m.entities: 109 | print(entity) 110 | if (type(entity) is MessageEntityUrl or type(entity) is MessageEntityTextUrl) and hasattr(entity, 'url'): 111 | print('add url') 112 | m.message = m.message[:entity.offset+entity.length+offset] + \ 113 | ' ('+entity.url+') ' + \ 114 | m.message[offset+entity.offset+entity.length:] 115 | offset = offset + len(entity.url) + 4 116 | elif (type(entity) is MessageEntityUrl): 117 | print('other url') 118 | # if m.message[entity.offset+offset:entity.offset+offset+entity.length].find('http') == -1: 119 | # m.message = m.message[:entity.offset+offset] + \ 120 | # 'https://' + m.message[entity.offset+offset:] 121 | # offset = offset + 8 122 | # else: 123 | # print('had http') 124 | elif (type(entity) is MessageEntityBold): 125 | print('bold') 126 | m.message = m.message[:entity.offset+offset] + '**' + m.message[entity.offset+offset:entity.offset + 127 | offset+entity.length-1] + '**' + m.message[entity.offset+offset+entity.length-1:] 128 | offset = offset + 4 129 | elif (type(entity) is MessageEntityItalic): 130 | print('italic') 131 | m.message = m.message[:entity.offset+offset] + '_' + m.message[entity.offset + 132 | offset:entity.offset+offset+entity.length] + '_' + m.message[entity.offset+offset+entity.length:] 133 | offset = offset + 2 134 | elif (type(entity) is MessageEntityUnderline): 135 | print('underline') 136 | m.message = m.message[:entity.offset+offset] + '__' + m.message[entity.offset + 137 | offset:entity.offset+offset+entity.length] + '__' + m.message[entity.offset+offset+entity.length:] 138 | offset = offset + 4 139 | elif (type(entity) is MessageEntityStrike): 140 | print('strike') 141 | m.message = m.message[:entity.offset+offset] + '~~' + m.message[entity.offset + 142 | offset:entity.offset+offset+entity.length] + '~~' + m.message[entity.offset+offset+entity.length:] 143 | offset = offset + 4 144 | elif (type(entity) is MessageEntityCode): 145 | print('code') 146 | m.message = m.message[:entity.offset+offset] + '`' + m.message[entity.offset + 147 | offset:entity.offset+offset+entity.length] + '`' + m.message[entity.offset+offset+entity.length:] 148 | offset = offset + 2 149 | # print(m.message) 150 | print('improved message') 151 | except: 152 | print('weird message') 153 | 154 | # gendersternchen 155 | m.message = m.message.replace('*', '\*') 156 | #offset = offset + m.message.count('\*') 157 | m.message = m.message.replace('\*\*', '**') 158 | 159 | return m.message 160 | 161 | 162 | print('started') 163 | 164 | with tg_client: 165 | # client.loop.run_until_complete(main()) 166 | tg_client.run_until_disconnected() 167 | --------------------------------------------------------------------------------