├── .gitignore
├── LICENSE
├── README.md
├── config_example.py
├── create_db.sql
├── dbhelper.py
├── sample_custom_post.py
└── sample_hourly.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .git/
2 | __pycache__/
3 | .idea/
4 | config.py
5 | storage.db
6 | static/atom.xml
7 | static/rss.xml
8 | test_feedgen.py
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016-2019 Evgeny Petrov
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 | # Create RSS/Atom feed for Telegram channel
2 |
3 | ⚠️ Warning! This code was made a long time ago, lots of API changes happened since then, making some parts of this code useless. For example, bots can already read messages in channels.
4 |
5 | Update 16 May 2019: It seems that Telegram now allows to read public channels in browser (https://t.me/tgbeta/3618) even without Telegram account. So this repo now goes read-only and just for historical purposes. So long and thanks for all the fish!
6 |
7 |
8 | This code allows you to form an RSS feed when you post messages to channel via bot. It works fine for me, but there can be some bugs.
9 | In theory, devs can use this to form RSS/Atom feed for their channels to make content available outside Telegram.
10 |
11 | ## Prerequisites
12 | * Python 3;
13 | * [pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI/) lib to work with Telegram [Bot API](https://core.telegram.org/bots/api);
14 | * [pytz](http://pytz.sourceforge.net/) library to work with timezones;
15 | * [feedgen](https://github.com/lkiesow/python-feedgen) library to create RSS/Atom feeds;
16 | * [CherryPy](http://www.cherrypy.org/) Micro web-server (to use webhooks);
17 | * sqlite3;
18 | * (optional) [nginx](http://nginx.org/) as reverse-proxy
19 |
20 | ## Installation
21 | To install feedgen you need 1024+ megabytes of RAM, also you need to install some libs first:
22 |
23 | ```
24 | sudo apt-get install sqlite3 libxml2 libxml2-dev libxslt1.1 libxslt1.1-dev
25 | pip3 install feedgen pytz pytelegrambotapi cherrypy
26 | sqlite3 storage.db < create_db.sql
27 | ```
28 |
29 | 1. Rename `config_example.py` to `config.py` (**Important**)
30 | 2. Fill in the necessary fields in `config.py` file
31 | 3. Launch bot using webhooks with self-signed certificate (`sample_custom_post.py`) or check sample auto-posting bot (`sample_hourly.py`)
32 |
33 | Check that your bot is set as admin in your channel!
34 | If using custom poster, open chat with your bot and write a message to it. You should see that message in both channel and RSS/Atom feed.
35 |
36 | ## Notes and restrictions
37 |
38 | * It's still unknown, why some RSS Parsers take too long to find updates in feed (though [QuiteRSS](https://quiterss.org/en/node) for Windows finds updates instantly)
39 | * In this version of code, you can only remove one entry from feed at a time. No way to also remove message from channel, only manually with `/remlast` command (Restriction of Bot API).
40 | * Only text messages are supported.
41 | * `parse_mode` argument is set to "HTML" by default (to be compatible with real RSS readers). You can disable it in code or change to "Markdown".
42 |
43 | ## Any questions?
44 | The code provided here should be self-explanatory, but in case you get stuck somewhere, feel free to e-mail me: groosha @ protonmail.com (remove extra spaces)
45 |
--------------------------------------------------------------------------------
/config_example.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | channel_id = "@yourchannel" # @username for public channels / chat_id for private channels
4 | token = "token" # Your bot's token from @BotFather
5 | db_name = "storage.db" # Name of SQL Database to use
6 | number_of_entries = 15 # How many entries will be included in RSS
7 | admin_ids = [111111, 2222222] # Allow only people with these IDs to post messages to channel via bot.
8 | webhook_url = "https://example.com/mywebhook/" # URL to set webhook
9 |
10 | # --- RSS Config values ---
11 | author_name = "Mr Jack" # Your name
12 | author_email = "jack@example.com" # Your e-mail
13 | timezone = 'GMT' # Find yours from here: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
14 | bot_link = "https://telegram.me/yourchannel" # Using your bot URL as ID and Link
15 | feed_link = "https://example.com/rss/atom.xml" # Self link to RSS/Atom
16 | feed_description = "Your Description"
17 | feed_title = "Your title"
18 | feed_language = "en" # Optional feed language value
--------------------------------------------------------------------------------
/create_db.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS texts;
2 |
3 | CREATE TABLE texts(
4 | id INTEGER PRIMARY KEY AUTOINCREMENT,
5 | post_text TEXT,
6 | post_date TEXT,
7 | post_id TEXT
8 | );
--------------------------------------------------------------------------------
/dbhelper.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import sqlite3
4 | from config import db_name, number_of_entries
5 | from time import time
6 |
7 | def insert(text, date, pid):
8 | with sqlite3.connect(db_name) as connection:
9 | connection.execute("INSERT INTO texts(post_text, post_date, post_id) values (?, ?, ?)", (text, date, pid,))
10 |
11 |
12 | def get_latest_entries():
13 | with sqlite3.connect(db_name) as connection:
14 | cursor = connection.cursor()
15 | entries = [dict(ptext=row[0], pdate=row[1], pid=row[2]) for row in cursor.execute("SELECT post_text, post_date, post_id FROM texts ORDER BY id DESC LIMIT ?", (number_of_entries,)).fetchall()]
16 | return entries
17 |
18 | def remove_last_entry():
19 | with sqlite3.connect(db_name) as connection:
20 | connection.execute("DELETE from texts WHERE id in (SELECT id from texts ORDER BY id DESC LIMIT 1);")
21 |
22 |
23 | def remove_old_entries():
24 | """
25 | Removes the oldest entries except the number set in config
26 | """
27 | with sqlite3.connect(db_name) as connection:
28 | connection.execute("DELETE FROM texts WHERE id IN (SELECT id FROM texts ORDER BY id DESC LIMIT -1 OFFSET ? )",(number_of_entries,))
29 |
30 |
31 |
--------------------------------------------------------------------------------
/sample_custom_post.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | This is an example of using Feedgen and pyTelegramBotAPI to send messages to channel
6 | as well as creating an RSS Feed.
7 | Note that to add new items to feed, you need to post via bot (not directly to channel).
8 |
9 | This example uses Webhooks method.
10 | """
11 |
12 | import cherrypy
13 | import pytz
14 | from datetime import datetime
15 | from feedgen.feed import FeedGenerator
16 | from email.utils import formatdate
17 | import random
18 | import telebot
19 | import config
20 | import dbhelper
21 | from cherrypy.lib.static import serve_file
22 |
23 | bot = telebot.TeleBot(config.token)
24 |
25 |
26 | WEBHOOK_HOST = ''
27 | WEBHOOK_PORT = 8443 # 443, 80, 88 or 8443 (port need to be 'open')
28 | WEBHOOK_LISTEN = '0.0.0.0' # In some VPS you may need to put here the IP addr
29 |
30 | WEBHOOK_SSL_CERT = './webhook_cert.pem' # Path to the ssl certificate
31 | WEBHOOK_SSL_PRIV = './webhook_pkey.pem' # Path to the ssl private key
32 |
33 | """
34 | Quick'n'dirty SSL certificate generation:
35 |
36 | openssl genrsa -out webhook_pkey.pem 2048
37 | openssl req -new -x509 -days 3650 -key webhook_pkey.pem -out webhook_cert.pem
38 |
39 | When asked for "Common Name (e.g. server FQDN or YOUR name)" you should reply
40 | with the same value in you put in WEBHOOK_HOST
41 | """
42 |
43 | WEBHOOK_URL_BASE = "https://%s:%s" % (WEBHOOK_HOST, WEBHOOK_PORT)
44 | WEBHOOK_URL_PATH = "/%s/" % (API_TOKEN)
45 |
46 |
47 | @bot.message_handler(commands=["remlast"])
48 | def remove_last_entry(message):
49 | if message.chat.id in config.admin_ids:
50 | dbhelper.remove_last_entry()
51 | generate_feed()
52 |
53 |
54 | # Handle only text messages
55 | @bot.message_handler(func=lambda message: message.chat.id in config.admin_ids, content_types=["text"])
56 | def my_text(message):
57 | # We don't need this crap in channel
58 | if message.text == "/start":
59 | return
60 | tz = pytz.timezone(config.timezone)
61 | msg_to_channel = bot.send_message(config.channel_id, message.text, parse_mode="HTML")
62 | dbhelper.insert(message.text.replace("\n","
"), formatdate(datetime.timestamp(datetime.now(tz)), localtime=True), msg_to_channel.message_id)
63 | generate_feed()
64 |
65 |
66 | class WebhookServer(object):
67 | @cherrypy.expose
68 | def index(self):
69 | length = int(cherrypy.request.headers['content-length'])
70 | json_string = cherrypy.request.body.read(length)
71 | json_string = json_string.decode("utf-8")
72 | update = telebot.types.Update.de_json(json_string)
73 | if update.message:
74 | bot.process_new_messages([update.message])
75 | if update.inline_query:
76 | bot.process_new_inline_query([update.inline_query])
77 |
78 | @cherrypy.expose
79 | def rss(self):
80 | # Return static file with ATOM feed
81 | return serve_file("/bots/telegram-feedgen-bot/static/atom.xml")
82 |
83 |
84 | def generate_feed():
85 | tz = pytz.timezone(config.timezone)
86 | # Get latest X entries from database
87 | entries = dbhelper.get_latest_entries()
88 |
89 | fg = FeedGenerator()
90 | # Feed id
91 | fg.id(config.bot_link)
92 | # Creator info (for Atom)
93 | fg.author(name=config.author_name, email=config.author_email, replace=True )
94 | # Self link to the feed
95 | fg.link(href=config.feed_link, rel='self')
96 | # Set description of your feed
97 | fg.description(config.feed_description)
98 | # Last time feed updated (use system time with timezone)
99 | fg.lastBuildDate(formatdate(datetime.timestamp(datetime.now(tz)), localtime=True))
100 | fg.title(config.feed_title)
101 | # Set time-to-live (I really don't know why set this)
102 | fg.ttl(5)
103 | # Does this parameter mean anything?
104 | fg.language(config.feed_language)
105 |
106 | for entry in entries:
107 | item = fg.add_entry()
108 | # Use message id to form valid URL (new feature in Telegram since Feb 2016)
109 | item.id("{!s}".format(entry["pid"]))
110 | item.link(href="{!s}/{!s}".format(config.bot_link, entry["pid"]), rel="alternate")
111 | # Set title and content from message text
112 | item.title(entry["ptext"])
113 | item.content(entry["ptext"])
114 | # Set publish/update datetime
115 | item.pubdate(entry["pdate"])
116 | item.updated(entry["pdate"])
117 |
118 | # Write RSS/Atom feed to file
119 | # It's preferred to have only one type at a time (or just create two functions)
120 | fg.atom_file('static/atom.xml')
121 | # fg.rss_file('static/rss.xml')
122 |
123 | if __name__ == '__main__':
124 |
125 | # Remove webhook, it fails sometimes the set if there is a previous webhook
126 | bot.remove_webhook()
127 |
128 | # Set webhook
129 | bot.set_webhook(url=WEBHOOK_URL_BASE+WEBHOOK_URL_PATH,
130 | certificate=open(WEBHOOK_SSL_CERT, 'r'))
131 |
132 | print("Webhook set")
133 |
134 | # Start cherrypy server
135 | cherrypy.config.update({
136 | 'server.socket_host': WEBHOOK_LISTEN,
137 | 'server.socket_port': WEBHOOK_PORT,
138 | 'server.ssl_module': 'builtin',
139 | 'server.ssl_certificate': WEBHOOK_SSL_CERT,
140 | 'server.ssl_private_key': WEBHOOK_SSL_PRIV,
141 | 'engine.autoreload.on': False
142 | })
143 |
144 | cherrypy.quickstart(WebhookServer(), '/', {'/': {}})
145 |
--------------------------------------------------------------------------------
/sample_hourly.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | This is an example of using Feedgen and pyTelegramBotAPI to send messages to channel
6 | as well as creating an RSS Feed.
7 |
8 | Automatically posts current time every hour to your channel and updates RSS.
9 | You can set this script to a cron: 0 * * * * cd /path/to/your/bot/ && python3 sample_hourly.py
10 | """
11 |
12 | from time import gmtime
13 | import telebot
14 | import config
15 | import pytz
16 | from datetime import datetime
17 | from feedgen.feed import FeedGenerator
18 | from email.utils import formatdate
19 | import dbhelper
20 |
21 | bot = telebot.TeleBot(config.token)
22 |
23 |
24 | def generate_feed():
25 | tz = pytz.timezone(config.timezone)
26 | # Get latest X entries from database
27 | entries = dbhelper.get_latest_entries()
28 |
29 | fg = FeedGenerator()
30 | # Feed id
31 | fg.id(config.bot_link)
32 | # Creator info (for Atom)
33 | fg.author(name=config.author_name, email=config.author_email, replace=True )
34 | # Self link to the feed
35 | fg.link(href=config.feed_link, rel='self')
36 | # Set description of your feed
37 | fg.description(config.feed_description)
38 | # Last time feed updated (use system time with timezone)
39 | fg.lastBuildDate(formatdate(datetime.timestamp(datetime.now(tz)), localtime=True))
40 | fg.title(config.feed_title)
41 | # Set time-to-live (I really don't know why set this)
42 | fg.ttl(5)
43 | # Does this parameter mean anything?
44 | fg.language(config.feed_language)
45 |
46 | for entry in entries:
47 | item = fg.add_entry()
48 | # Use message id to form valid URL (new feature in Telegram since Feb 2016)
49 | item.id("{!s}".format(entry["pid"]))
50 | item.link(href="{!s}/{!s}".format(config.bot_link, entry["pid"]), rel="alternate")
51 | # Set title and content from message text
52 | item.title(entry["ptext"])
53 | item.content(entry["ptext"])
54 | # Set publish/update datetime
55 | item.pubdate(entry["pdate"])
56 | item.updated(entry["pdate"])
57 |
58 | # Write RSS/Atom feed to file
59 | # It's preferred to have only one type at a time (or just create two functions)
60 | fg.atom_file('static/atom.xml')
61 | # fg.rss_file('static/rss.xml')
62 |
63 | if __name__ == '__main__':
64 | message = "It\'s {!s} o\'clock! (GMT)".format(gmtime().tm_hour)
65 | msg = bot.send_message(config.channel_id, message, disable_notification=True, parse_mode="HTML")
66 | tz = pytz.timezone(config.timezone)
67 | dbhelper.insert(message, formatdate(datetime.timestamp(datetime.now(tz)), localtime=True), msg.message_id)
68 | generate_feed()
69 |
--------------------------------------------------------------------------------