├── .github └── workflows │ └── cron.yml ├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── rss2telegram.db └── rss2telegram.py /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Cron Action 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 */1 * * *' 6 | 7 | 8 | jobs: 9 | rss2telegram: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 10 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v4 15 | - name: Download artifact 16 | id: download 17 | continue-on-error: true 18 | uses: dawidd6/action-download-artifact@v6 19 | with: 20 | workflow: cron.yml 21 | workflow_conclusion: success 22 | - name: Move database 23 | continue-on-error: true 24 | run: mv rss2telegram/rss2telegram.db rss2telegram.db 25 | - name: Setup Python 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: '3.8' 29 | - name: Install Requirements 30 | run: pip install -r requirements.txt 31 | - name: Run Rss to Telegram 32 | run: python rss2telegram.py 33 | env: 34 | BOT_TOKEN: ${{ secrets.BOT_TOKEN }} 35 | DESTINATION: ${{ vars.DESTINATION }} 36 | URL: ${{ vars.URL }} 37 | EMOJIS: ${{ vars.EMOJIS }} 38 | TOPIC: ${{ vars.TOPIC }} 39 | HIDE_BUTTON: ${{ vars.HIDE_BUTTON }} 40 | MESSAGE_TEMPLATE: ${{ vars.MESSAGE_TEMPLATE }} 41 | BUTTON_TEXT: ${{ vars.BUTTON_TEXT }} 42 | PARAMETERS: ${{ vars.PARAMETERS }} 43 | TELEGRAPH_TOKEN: ${{ vars.TELEGRAPH_TOKEN }} 44 | DRYRUN: ${{ steps.download.outcome }} 45 | - name: Upload history 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: rss2telegram 49 | path: rss2telegram.db 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RSS Logo 2 | 3 | # Rss2Telegram 4 | 5 | Envio automático de feed RSS para pessoa, canal ou grupo no Telegram. 6 | 7 | [Mais explicações e exemplos de uso aqui](https://blog.gabrf.com/posts/Rss2Telegram/). 8 | 9 | ## Participe: 10 | 11 | Participe das conversas sobre o projeto na aba [Discussions](https://github.com/GabrielRF/Rss2Telegram/discussions). 12 | 13 | Issues também são sempre bem vindas. 14 | 15 | ## Configuração: 16 | 17 | Defina as variáveis na aba `Secrets` do repositório: 18 | 19 | `BOT_TOKEN`: Token do bot que enviará as mensagens no canal ([@BotFather](https://t.me/BotFather)); 20 | 21 | Defina as variáveis na aba `Variables` do repositório: 22 | 23 | `DESTINATION`: Destinos das mensagens separados por vírgulas (`@destino` ou ID). Opcionalmente, remova a variável e crie um arquivo de nome `DESTINATION.txt` com os valores; 24 | 25 | `URL`: Endereços de feeds RSS, separados por "enter", ou seja, um por linha. Opcionalmente, remova a variável e crie um arquivo de nome `URL.txt` com os valores; 26 | 27 | `PARAMETERS`: (opcional) Parâmetros que serão adicionados ao fim do link; 28 | 29 | `MESSAGE_TEMPLATE`: (opcional) Texto da mensagem. Valor padrão: `{TITLE}` ([ver opções](#opções-de-variáveis)); 30 | 31 | `BUTTON_TEXT`: (opcional) Texto do botão com o link. Sugestão: `{SITE_NAME}`. Se esta variável não for criada não será enviado um botão. ([Ver opções](#opções-de-variáveis)); 32 | 33 | `EMOJIS`: (opcional) Emojis separados por vírgulas. Podem ser usados na mensagem ou no botão; 34 | 35 | `TOPIC`: (opcional) ID do tópico em que a mensagem será enviada. Necessário para grupos com a opção de tópicos ativada. [Como obter um ID de um tópico](#id-de-tópico) 36 | 37 | `TELEGRAPH_TOKEN`: (opcional) Chave para acesso ao Telegraph. [Como obter uma chave Telegraph](#chave-telegraph) 38 | 39 | `HIDE_BUTTON`: (opcional) Caso definida, desabilita o botão no envio, permitindo assim a existência do `Leitura Rápida`. 40 | 41 | ### Opções de variáveis 42 | 43 | `{SITE_NAME}`: Nome do site; 44 | 45 | `{TITLE}`: Título do post; 46 | 47 | `{SUMMARY}`: Sumário do post; 48 | 49 | `{LINK}`: Link do post; 50 | 51 | `{EMOJI}`: Emoji escolhido aleatoriamente da lista. 52 | 53 | ## Filtros 54 | 55 | Por padrão, todos os elementos do feed RSS serão enviados. Caso queira filtrar o conteúdo, crie um arquivo chamado `RULES.txt` e adicione as regras desejadas ao arquivo. As regras serão executadas em ordem! 56 | 57 | > O valor contido em termo funcionará independente de letras maiúsculas ou minúsculas. 58 | 59 | `ACCEPT:ALL`: Todas as mensagens serão enviadas; 60 | 61 | `DROP:ALL`: Todas as mensagens não serão enviadas; 62 | 63 | `ACCEPT:termo`: A mensagem será enviada se `termo` estiver presente; 64 | 65 | `DROP:termo`: A mensagem não será enviada se `termo` estiver presente. 66 | 67 | ### Exemplos de Filtros: 68 | 69 | 1. Todos as mensagens serão enviadas, menos as que tiverem o termo `política`: 70 | 71 | ``` 72 | ACCEPT:ALL 73 | DROP:Política 74 | ``` 75 | 76 | 2. Nenhuma mensagem será enviada, com exceção das mensagens com os termos `futebol` e `vôlei`: 77 | 78 | ``` 79 | DROP:ALL 80 | ACCEPT:futebol 81 | ACCEPT:vôlei 82 | ``` 83 | 84 | ## Uso 85 | 86 | Faça um *Fork*, defina as variáveis e habilite a ação em "*Enable workflow*". Pronto! 87 | 88 | ![Enable Workflow](https://user-images.githubusercontent.com/7331540/178158090-bf774cae-071b-4ac2-ab03-9c5c1132b79e.png) 89 | 90 | A ação irá buscar as atualizações a cada hora conforme definido no arquivo [cron.yml](.github/workflows/cron.yml). 91 | 92 | ## ID de tópico 93 | 94 | Caso o grupo tenha a opção de tópicos ativada, será necessário indicar em qual tópico a mensagem será enviada. Isto é feito usando-se a variável `TOPIC`. A maneira mais fácil de se obter um ID de um tópico é copiando o link de uma mensagem de um tópico. O ID será o penúltimo número do link. 95 | 96 | Exemplo: O link para uma mensagem de um tópico seria `https://t.me/c/987654321/123/4567`. Neste caso, `123` seria o ID do tópico, o número que deveria ser colocado na variável. 97 | 98 | ## Chave Telegraph 99 | 100 | > Atenção: Caso a variável TELEGRAPH_TOKEN esteja definida, o post não terá botão ou imagem, pois ambos não permitiriam a existência da opção "Visualização Rápida". 101 | 102 | Para criar sua chave de acesso ao Telegraph e gerar a Visualização Rápida de qualquer site, acesse: 103 | 104 | ``` 105 | https://api.telegra.ph/createAccount?short_name=&author_name= 106 | ``` 107 | 108 | * `SHORT_NAME`: Uma abreviação de seu nome; 109 | 110 | * `AUTHOR_NAME`: Seu nome. 111 | 112 | A resposta do site será algo como: 113 | 114 | ``` 115 | { 116 | "ok": true, 117 | "result": { 118 | "short_name": "NOME", 119 | "author_name": "NOME", 120 | "author_url": "", 121 | "access_token": "abcdefghijklmnopqrtuvxz123456789", 122 | "auth_url": "https://edit.telegra.ph/auth/123456789012345678901234567890" 123 | } 124 | } 125 | ``` 126 | 127 | O valor presente em `access_token` é o valor a ser usado na variável `TELEGRAPH_TOKEN`. 128 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyTelegramBotAPI==4.5.1 2 | requests==2.28.0 3 | feedparser==6.0.10 4 | bs4==0.0.1 5 | beautifulsoup4==4.11.1 6 | telegraph==1.4.1 7 | -------------------------------------------------------------------------------- /rss2telegram.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielRF/Rss2Telegram/024df483cab056b8636aa76dc9366699b26058bb/rss2telegram.db -------------------------------------------------------------------------------- /rss2telegram.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from telebot import types 3 | from time import gmtime 4 | import feedparser 5 | import os 6 | import re 7 | import telebot 8 | import telegraph 9 | import time 10 | import random 11 | import requests 12 | import sqlite3 13 | 14 | def get_variable(variable): 15 | if not os.environ.get(f'{variable}'): 16 | var_file = open(f'{variable}.txt', 'r') 17 | return var_file.read() 18 | return os.environ.get(f'{variable}') 19 | 20 | URL = get_variable('URL') 21 | DESTINATION = get_variable('DESTINATION') 22 | BOT_TOKEN = os.environ.get('BOT_TOKEN') 23 | EMOJIS = os.environ.get('EMOJIS', '🗞,📰,🗒,🗓,📋,🔗,📝,🗃') 24 | PARAMETERS = os.environ.get('PARAMETERS', False) 25 | HIDE_BUTTON = os.environ.get('HIDE_BUTTON', False) 26 | DRYRUN = os.environ.get('DRYRUN') 27 | TOPIC = os.environ.get('TOPIC', False) 28 | TELEGRAPH_TOKEN = os.environ.get('TELEGRAPH_TOKEN', False) 29 | 30 | bot = telebot.TeleBot(BOT_TOKEN) 31 | 32 | def add_to_history(link): 33 | conn = sqlite3.connect('rss2telegram.db') 34 | cursor = conn.cursor() 35 | aux = f'INSERT INTO history (link) VALUES ("{link}")' 36 | cursor.execute(aux) 37 | conn.commit() 38 | conn.close() 39 | 40 | def check_history(link): 41 | conn = sqlite3.connect('rss2telegram.db') 42 | cursor = conn.cursor() 43 | aux = f'SELECT * from history WHERE link="{link}"' 44 | cursor.execute(aux) 45 | data = cursor.fetchone() 46 | conn.close() 47 | return data 48 | 49 | def firewall(text): 50 | try: 51 | rules = open(f'RULES.txt', 'r') 52 | except FileNotFoundError: 53 | return True 54 | result = None 55 | for rule in rules.readlines(): 56 | opt, arg = rule.split(':') 57 | arg = arg.strip() 58 | if arg == 'ALL' and opt == 'DROP': 59 | result = False 60 | elif arg == 'ALL' and opt == 'ACCEPT': 61 | result = True 62 | elif arg.lower() in text.lower() and opt == 'DROP': 63 | result = False 64 | elif arg.lower() in text.lower() and opt == 'ACCEPT': 65 | result = True 66 | return result 67 | 68 | def create_telegraph_post(topic): 69 | telegraph_auth = telegraph.Telegraph( 70 | access_token=f'{get_variable("TELEGRAPH_TOKEN")}' 71 | ) 72 | response = telegraph_auth.create_page( 73 | f'{topic["title"]}', 74 | html_content=( 75 | f'{topic["summary"]}

' 76 | + f'Ver original ({topic["site_name"]})' 77 | ), 78 | author_name=f'{topic["site_name"]}' 79 | ) 80 | return response["url"] 81 | 82 | def send_message(topic, button): 83 | if DRYRUN == 'failure': 84 | return 85 | 86 | MESSAGE_TEMPLATE = os.environ.get(f'MESSAGE_TEMPLATE', False) 87 | 88 | if MESSAGE_TEMPLATE: 89 | MESSAGE_TEMPLATE = set_text_vars(MESSAGE_TEMPLATE, topic) 90 | else: 91 | MESSAGE_TEMPLATE = f'{topic["title"]}' 92 | 93 | if TELEGRAPH_TOKEN: 94 | iv_link = create_telegraph_post(topic) 95 | MESSAGE_TEMPLATE = f'󠀠{MESSAGE_TEMPLATE}' 96 | 97 | if not firewall(str(topic)): 98 | print(f'xxx {topic["title"]}') 99 | return 100 | 101 | btn_link = button 102 | if button: 103 | btn_link = types.InlineKeyboardMarkup() 104 | btn = types.InlineKeyboardButton(f'{button}', url=topic['link']) 105 | btn_link.row(btn) 106 | 107 | if HIDE_BUTTON or TELEGRAPH_TOKEN: 108 | for dest in DESTINATION.split(','): 109 | bot.send_message(dest, MESSAGE_TEMPLATE, parse_mode='HTML', reply_to_message_id=TOPIC) 110 | else: 111 | if topic['photo'] and not TELEGRAPH_TOKEN: 112 | response = requests.get(topic['photo'], headers = {'User-agent': 'Mozilla/5.1'}) 113 | open('img', 'wb').write(response.content) 114 | for dest in DESTINATION.split(','): 115 | photo = open('img', 'rb') 116 | try: 117 | bot.send_photo(dest, photo, caption=MESSAGE_TEMPLATE, parse_mode='HTML', reply_markup=btn_link, reply_to_message_id=TOPIC) 118 | except telebot.apihelper.ApiTelegramException: 119 | topic['photo'] = False 120 | send_message(topic, button) 121 | else: 122 | for dest in DESTINATION.split(','): 123 | bot.send_message(dest, MESSAGE_TEMPLATE, parse_mode='HTML', reply_markup=btn_link, disable_web_page_preview=True, reply_to_message_id=TOPIC) 124 | print(f'... {topic["title"]}') 125 | time.sleep(0.2) 126 | 127 | def get_img(url): 128 | try: 129 | response = requests.get(url, headers = {'User-agent': 'Mozilla/5.1'}, timeout=3) 130 | html = BeautifulSoup(response.content, 'html.parser') 131 | photo = html.find('meta', {'property': 'og:image'})['content'] 132 | except TypeError: 133 | photo = False 134 | except requests.exceptions.ReadTimeout: 135 | photo = False 136 | except requests.exceptions.TooManyRedirects: 137 | photo = False 138 | return photo 139 | 140 | def define_link(link, PARAMETERS): 141 | if PARAMETERS: 142 | if '?' in link: 143 | return f'{link}&{PARAMETERS}' 144 | return f'{link}?{PARAMETERS}' 145 | return f'{link}' 146 | 147 | 148 | 149 | def set_text_vars(text, topic): 150 | cases = { 151 | 'SITE_NAME': topic['site_name'], 152 | 'TITLE': topic['title'], 153 | 'SUMMARY': re.sub('<[^<]+?>', '', topic['summary']), 154 | 'LINK': define_link(topic['link'], PARAMETERS), 155 | 'EMOJI': random.choice(EMOJIS.split(",")) 156 | } 157 | for word in re.split('{|}', text): 158 | try: 159 | text = text.replace(word, cases.get(word)) 160 | except TypeError: 161 | continue 162 | return text.replace('\\n', '\n').replace('{', '').replace('}', '') 163 | 164 | 165 | def check_topics(url): 166 | now = gmtime() 167 | feed = feedparser.parse(url) 168 | try: 169 | source = feed['feed']['title'] 170 | except KeyError: 171 | print(f'\nERRO: {url} não parece um feed RSS válido.') 172 | return 173 | print(f'\nChecando {source}:{url}') 174 | for tpc in reversed(feed['items'][:10]): 175 | if check_history(tpc.links[0].href): 176 | continue 177 | add_to_history(tpc.links[0].href) 178 | topic = {} 179 | topic['site_name'] = feed['feed']['title'] 180 | topic['title'] = tpc.title.strip() 181 | topic['summary'] = tpc.summary 182 | topic['link'] = tpc.links[0].href 183 | topic['photo'] = get_img(tpc.links[0].href) 184 | BUTTON_TEXT = os.environ.get('BUTTON_TEXT', False) 185 | if BUTTON_TEXT: 186 | BUTTON_TEXT = set_text_vars(BUTTON_TEXT, topic) 187 | try: 188 | send_message(topic, BUTTON_TEXT) 189 | except telebot.apihelper.ApiTelegramException as e: 190 | print(e) 191 | pass 192 | 193 | if __name__ == "__main__": 194 | for url in URL.split(): 195 | check_topics(url) 196 | 197 | --------------------------------------------------------------------------------