diff --git a/slixfeed/action.py b/slixfeed/action.py index ef5185a..2ea52dd 100644 --- a/slixfeed/action.py +++ b/slixfeed/action.py @@ -498,22 +498,17 @@ def list_unread_entries(result, feed_title): link = result[2] link = remove_tracking_parameters(link) link = (replace_hostname(link, "link")) or link - news_item = ( - "\n{}\n{}\n{} [{}]\n" - ).format( - str(title), str(link), str(feed_title), str(ix) - ) + news_item = ("\n{}\n{}\n{} [{}]\n").format(str(title), str(link), + str(feed_title), str(ix)) return news_item def list_search_results(query, results): - message = ( - "Search results for '{}':\n\n```" - ).format(query) + message = ("Search results for '{}':\n\n```" + .format(query)) for result in results: - message += ( - "\n{}\n{}\n" - ).format(str(result[0]), str(result[1])) + message += ("\n{}\n{}\n" + .format(str(result[0]), str(result[1]))) if len(results): message += "```\nTotal of {} results".format(len(results)) else: @@ -523,15 +518,13 @@ def list_search_results(query, results): def list_feeds_by_query(db_file, query): results = sqlite.search_feeds(db_file, query) - message = ( - 'Feeds containing "{}":\n\n```' - .format(query)) + message = ('Feeds containing "{}":\n\n```' + .format(query)) for result in results: - message += ( - '\nName : {} [{}]' - '\nURL : {}' - '\n' - .format(str(result[0]), str(result[1]), str(result[2]))) + message += ('\nName : {} [{}]' + '\nURL : {}' + '\n' + .format(str(result[0]), str(result[1]), str(result[2]))) if len(results): message += "\n```\nTotal of {} feeds".format(len(results)) else: @@ -607,6 +600,15 @@ def list_last_entries(results, num): return message +def pick_a_feed(lang=None): + config_dir = config.get_default_config_directory() + with open(config_dir + '/' + 'feeds.toml', mode="rb") as feeds: + urls = tomllib.load(feeds) + import random + url = random.choice(urls['feeds']) + return url + + def list_feeds(results): message = "\nList of subscriptions:\n\n```\n" for result in results: @@ -621,10 +623,11 @@ def list_feeds(results): message += ('```\nTotal of {} subscriptions.\n' .format(len(results))) else: - message = ('List of subscriptions is empty.\n' - 'To add feed, send a URL\n' - 'Featured feed: ' - 'https://reclaimthenet.org/feed/') + url = pick_a_feed() + message = ('List of subscriptions is empty. To add a feed, send a URL.' + 'Featured feed:\n*{}*\n{}' + .format(url['name'], + url['link'])) return message @@ -678,8 +681,8 @@ def export_to_opml(jid, filename, results): async def import_opml(db_file, url): result = await fetch.http(url) - document = result[0] - if document: + if not result['error']: + document = result['content'] root = ET.fromstring(document) before = await sqlite.get_number_of_items( db_file, 'feeds') @@ -703,9 +706,9 @@ async def add_feed(db_file, url): exist = await sqlite.get_feed_id_and_name(db_file, url) if not exist: result = await fetch.http(url) - document = result[0] - status_code = result[1] - if document: + if not result['error']: + document = result['content'] + status_code = result['status_code'] feed = parse(document) # if is_feed(url, feed): if is_feed(feed): @@ -745,7 +748,7 @@ async def add_feed(db_file, url): feed_id = await sqlite.get_feed_id(db_file, url) feed_id = feed_id[0] await sqlite.mark_feed_as_read(db_file, feed_id) - result_final = {'url' : url, + result_final = {'link' : url, 'index' : feed_id, 'name' : title, 'code' : status_code, @@ -795,7 +798,7 @@ async def add_feed(db_file, url): feed_id = await sqlite.get_feed_id(db_file, url) feed_id = feed_id[0] await sqlite.mark_feed_as_read(db_file, feed_id) - result_final = {'url' : url, + result_final = {'link' : url, 'index' : feed_id, 'name' : title, 'code' : status_code, @@ -808,15 +811,26 @@ async def add_feed(db_file, url): else: # NOTE Do not be tempted to return a compact dictionary. # That is, dictionary within dictionary - # Return multimple dictionaries. + # Return multiple dictionaries in a list or tuple. result = await crawl.probe_page(url, document) - if isinstance(result, list): + if not result: + # Get out of the loop with dict indicating error. + result_final = {'link' : url, + 'index' : None, + 'name' : None, + 'code' : status_code, + 'error' : True, + 'exist' : False} + break + elif isinstance(result, list): + # Get out of the loop and deliver a list of dicts. result_final = result break else: - url = result['url'] + # Go back up to the while loop and try again. + url = result['link'] else: - result_final = {'url' : url, + result_final = {'link' : url, 'index' : None, 'name' : None, 'code' : status_code, @@ -828,7 +842,7 @@ async def add_feed(db_file, url): else: ix = exist[0] name = exist[1] - result_final = {'url' : url, + result_final = {'link' : url, 'index' : ix, 'name' : name, 'code' : None, @@ -854,145 +868,142 @@ async def scan_json(db_file, url): """ if isinstance(url, tuple): url = url[0] result = await fetch.http(url) - try: - document = result[0] - status = result[1] - except: - return - new_entries = [] - if document and status == 200: - feed = json.loads(document) - entries = feed["items"] - await remove_nonexistent_entries_json( - db_file, url, feed) - try: - feed_id = await sqlite.get_feed_id(db_file, url) - feed_id = feed_id[0] - # await sqlite.update_feed_validity( - # db_file, feed_id, valid) - if "date_published" in feed.keys(): - updated = feed["date_published"] - try: - updated = dt.convert_struct_time_to_iso8601(updated) - except: + if not result['error']: + document = result['content'] + status = result['status_code'] + new_entries = [] + if document and status == 200: + feed = json.loads(document) + entries = feed["items"] + await remove_nonexistent_entries_json( + db_file, url, feed) + try: + feed_id = await sqlite.get_feed_id(db_file, url) + feed_id = feed_id[0] + # await sqlite.update_feed_validity( + # db_file, feed_id, valid) + if "date_published" in feed.keys(): + updated = feed["date_published"] + try: + updated = dt.convert_struct_time_to_iso8601(updated) + except: + updated = '' + else: updated = '' - else: - updated = '' + feed_id = await sqlite.get_feed_id(db_file, url) + feed_id = feed_id[0] + await sqlite.update_feed_properties( + db_file, feed_id, len(feed["items"]), updated) + # await update_feed_status + except ( + IncompleteReadError, + IncompleteRead, + error.URLError + ) as e: + logging.error(e) + return + # new_entry = 0 + for entry in entries: + if "date_published" in entry.keys(): + date = entry["date_published"] + date = dt.rfc2822_to_iso8601(date) + elif "date_modified" in entry.keys(): + date = entry["date_modified"] + date = dt.rfc2822_to_iso8601(date) + else: + date = dt.now() + if "url" in entry.keys(): + # link = complete_url(source, entry.link) + link = join_url(url, entry["url"]) + link = trim_url(link) + else: + link = url + # title = feed["feed"]["title"] + # title = "{}: *{}*".format(feed["feed"]["title"], entry.title) + title = entry["title"] if "title" in entry.keys() else date + entry_id = entry["id"] if "id" in entry.keys() else link + feed_id = await sqlite.get_feed_id(db_file, url) + feed_id = feed_id[0] + exist = await sqlite.check_entry_exist( + db_file, feed_id, entry_id=entry_id, + title=title, link=link, date=date) + if not exist: + summary = entry["summary"] if "summary" in entry.keys() else '' + if not summary: + summary = (entry["content_html"] + if "content_html" in entry.keys() + else '') + if not summary: + summary = (entry["content_text"] + if "content_text" in entry.keys() + else '') + read_status = 0 + pathname = urlsplit(link).path + string = ( + "{} {} {}" + ).format( + title, summary, pathname) + allow_list = config.is_include_keyword(db_file, "allow", + string) + if not allow_list: + reject_list = config.is_include_keyword(db_file, "deny", + string) + if reject_list: + read_status = 1 + logging.debug('Rejected : {}' + '\n' + 'Keyword : {}' + .format(link, reject_list)) + if isinstance(date, int): + logging.error('Variable "date" is int: {}'.format(date)) + media_link = '' + if "attachments" in entry.keys(): + for e_link in entry["attachments"]: + try: + # if (link.rel == "enclosure" and + # (link.type.startswith("audio/") or + # link.type.startswith("image/") or + # link.type.startswith("video/")) + # ): + media_type = e_link["mime_type"][:e_link["mime_type"].index("/")] + if media_type in ("audio", "image", "video"): + media_link = e_link["url"] + media_link = join_url(url, e_link["url"]) + media_link = trim_url(media_link) + break + except: + logging.info('KeyError: "url"\n' + 'Missing "url" attribute for {}' + .format(url)) + logging.info('Continue scanning for next ' + 'potential enclosure of {}' + .format(link)) + entry = { + "title": title, + "link": link, + "enclosure": media_link, + "entry_id": entry_id, + "date": date, + "read_status": read_status + } + new_entries.extend([entry]) + # await sqlite.add_entry( + # db_file, title, link, entry_id, + # url, date, read_status) + # await sqlite.set_date(db_file, url) + if len(new_entries): feed_id = await sqlite.get_feed_id(db_file, url) feed_id = feed_id[0] - await sqlite.update_feed_properties( - db_file, feed_id, len(feed["items"]), updated) - # await update_feed_status - except ( - IncompleteReadError, - IncompleteRead, - error.URLError - ) as e: - logging.error(e) - return - # new_entry = 0 - for entry in entries: - if "date_published" in entry.keys(): - date = entry["date_published"] - date = dt.rfc2822_to_iso8601(date) - elif "date_modified" in entry.keys(): - date = entry["date_modified"] - date = dt.rfc2822_to_iso8601(date) - else: - date = dt.now() - if "url" in entry.keys(): - # link = complete_url(source, entry.link) - link = join_url(url, entry["url"]) - link = trim_url(link) - else: - link = url - # title = feed["feed"]["title"] - # title = "{}: *{}*".format(feed["feed"]["title"], entry.title) - title = entry["title"] if "title" in entry.keys() else date - entry_id = entry["id"] if "id" in entry.keys() else link - feed_id = await sqlite.get_feed_id(db_file, url) - feed_id = feed_id[0] - exist = await sqlite.check_entry_exist( - db_file, feed_id, entry_id=entry_id, - title=title, link=link, date=date) - if not exist: - summary = entry["summary"] if "summary" in entry.keys() else '' - if not summary: - summary = (entry["content_html"] - if "content_html" in entry.keys() - else '') - if not summary: - summary = (entry["content_text"] - if "content_text" in entry.keys() - else '') - read_status = 0 - pathname = urlsplit(link).path - string = ( - "{} {} {}" - ).format( - title, summary, pathname) - allow_list = await config.is_include_keyword( - db_file, "allow", string) - if not allow_list: - reject_list = await config.is_include_keyword( - db_file, "deny", string) - if reject_list: - read_status = 1 - logging.debug( - "Rejected : {}\n" - "Keyword : {}".format( - link, reject_list)) - if isinstance(date, int): - logging.error( - "Variable 'date' is int: {}".format(date)) - media_link = '' - if "attachments" in entry.keys(): - for e_link in entry["attachments"]: - try: - # if (link.rel == "enclosure" and - # (link.type.startswith("audio/") or - # link.type.startswith("image/") or - # link.type.startswith("video/")) - # ): - media_type = e_link["mime_type"][:e_link["mime_type"].index("/")] - if media_type in ("audio", "image", "video"): - media_link = e_link["url"] - media_link = join_url(url, e_link["url"]) - media_link = trim_url(media_link) - break - except: - logging.info('KeyError: "url"\n' - 'Missing "url" attribute for {}' - .format(url)) - logging.info('Continue scanning for next ' - 'potential enclosure of {}' - .format(link)) - entry = { - "title": title, - "link": link, - "enclosure": media_link, - "entry_id": entry_id, - "date": date, - "read_status": read_status - } - new_entries.extend([entry]) - # await sqlite.add_entry( - # db_file, title, link, entry_id, - # url, date, read_status) - # await sqlite.set_date(db_file, url) - if len(new_entries): - feed_id = await sqlite.get_feed_id(db_file, url) - feed_id = feed_id[0] - await sqlite.add_entries_and_update_timestamp( - db_file, feed_id, new_entries) + await sqlite.add_entries_and_update_timestamp(db_file, feed_id, + new_entries) async def view_feed(url): while True: result = await fetch.http(url) - document = result[0] - status = result[1] - if document: + if not result['error']: + document = result['content'] + status = result['status_code'] feed = parse(document) # if is_feed(url, feed): if is_feed(feed): @@ -1023,13 +1034,12 @@ async def view_feed(url): date = dt.rfc2822_to_iso8601(date) else: date = "*** No date ***" - response += ( - "Title : {}\n" - "Date : {}\n" - "Link : {}\n" - "Count : {}\n" - "\n" - ).format(title, date, link, counter) + response += ("Title : {}\n" + "Date : {}\n" + "Link : {}\n" + "Count : {}\n" + "\n" + .format(title, date, link, counter)) if counter > 4: break response += ( @@ -1037,8 +1047,7 @@ async def view_feed(url): ).format(url) break else: - result = await crawl.probe_page( - url, document) + result = await crawl.probe_page(url, document) if isinstance(result, str): response = result break @@ -1054,9 +1063,9 @@ async def view_feed(url): async def view_entry(url, num): while True: result = await fetch.http(url) - document = result[0] - status = result[1] - if document: + if not result['error']: + document = result['content'] + status = result['status_code'] feed = parse(document) # if is_feed(url, feed): if is_feed(feed): @@ -1094,19 +1103,17 @@ async def view_entry(url, num): link = trim_url(link) else: link = "*** No link ***" - response = ( - "{}\n" - "\n" - # "> {}\n" - "{}\n" - "\n" - "{}\n" - "\n" - ).format(title, summary, link) + response = ("{}\n" + "\n" + # "> {}\n" + "{}\n" + "\n" + "{}\n" + "\n" + .format(title, summary, link)) break else: - result = await crawl.probe_page( - url, document) + result = await crawl.probe_page(url, document) if isinstance(result, str): response = result break @@ -1119,6 +1126,7 @@ async def view_entry(url, num): return response +# TODO Rename function name (idea: scan_and_populate) async def scan(db_file, url): """ Check feeds for new entries. @@ -1132,142 +1140,136 @@ async def scan(db_file, url): """ if isinstance(url, tuple): url = url[0] result = await fetch.http(url) - try: - document = result[0] - status = result[1] - except: - return - new_entries = [] - if document and status == 200: - feed = parse(document) - entries = feed.entries - # length = len(entries) - await remove_nonexistent_entries( - db_file, url, feed) - try: - if feed.bozo: - # bozo = ( - # "WARNING: Bozo detected for feed: {}\n" - # "For more information, visit " - # "https://pythonhosted.org/feedparser/bozo.html" - # ).format(url) - # print(bozo) - valid = 0 - else: - valid = 1 - feed_id = await sqlite.get_feed_id(db_file, url) - feed_id = feed_id[0] - await sqlite.update_feed_validity( - db_file, feed_id, valid) - if "updated_parsed" in feed["feed"].keys(): - updated = feed["feed"]["updated_parsed"] - try: - updated = dt.convert_struct_time_to_iso8601(updated) - except: + if not result['error']: + document = result['content'] + status = result['status_code'] + new_entries = [] + if document and status == 200: + feed = parse(document) + entries = feed.entries + # length = len(entries) + await remove_nonexistent_entries(db_file, url, feed) + try: + if feed.bozo: + # bozo = ( + # "WARNING: Bozo detected for feed: {}\n" + # "For more information, visit " + # "https://pythonhosted.org/feedparser/bozo.html" + # ).format(url) + # print(bozo) + valid = 0 + else: + valid = 1 + feed_id = await sqlite.get_feed_id(db_file, url) + feed_id = feed_id[0] + await sqlite.update_feed_validity( + db_file, feed_id, valid) + if "updated_parsed" in feed["feed"].keys(): + updated = feed["feed"]["updated_parsed"] + try: + updated = dt.convert_struct_time_to_iso8601(updated) + except: + updated = '' + else: updated = '' - else: - updated = '' + feed_id = await sqlite.get_feed_id(db_file, url) + feed_id = feed_id[0] + await sqlite.update_feed_properties(db_file, feed_id, + len(feed["entries"]), updated) + # await update_feed_status + except (IncompleteReadError, IncompleteRead, error.URLError) as e: + logging.error(e) + return + # new_entry = 0 + for entry in entries: + if entry.has_key("published"): + date = entry.published + date = dt.rfc2822_to_iso8601(date) + elif entry.has_key("updated"): + date = entry.updated + date = dt.rfc2822_to_iso8601(date) + else: + date = dt.now() + if entry.has_key("link"): + # link = complete_url(source, entry.link) + link = join_url(url, entry.link) + link = trim_url(link) + else: + link = url + # title = feed["feed"]["title"] + # title = "{}: *{}*".format(feed["feed"]["title"], entry.title) + title = entry.title if entry.has_key("title") else date + entry_id = entry.id if entry.has_key("id") else link + feed_id = await sqlite.get_feed_id(db_file, url) + feed_id = feed_id[0] + exist = await sqlite.check_entry_exist(db_file, feed_id, + entry_id=entry_id, + title=title, link=link, + date=date) + if not exist: + summary = entry.summary if entry.has_key("summary") else '' + read_status = 0 + pathname = urlsplit(link).path + string = ( + "{} {} {}" + ).format( + title, summary, pathname) + allow_list = config.is_include_keyword(db_file, "allow", + string) + if not allow_list: + reject_list = config.is_include_keyword(db_file, "deny", + string) + if reject_list: + read_status = 1 + logging.debug('Rejected : {}' + '\n' + 'Keyword : {}'.format(link, + reject_list)) + if isinstance(date, int): + logging.error('Variable "date" is int: {}' + .format(date)) + media_link = '' + if entry.has_key("links"): + for e_link in entry.links: + try: + # if (link.rel == "enclosure" and + # (link.type.startswith("audio/") or + # link.type.startswith("image/") or + # link.type.startswith("video/")) + # ): + media_type = e_link.type[:e_link.type.index("/")] + if e_link.has_key("rel"): + if (e_link.rel == "enclosure" and + media_type in ("audio", "image", "video")): + media_link = e_link.href + media_link = join_url(url, e_link.href) + media_link = trim_url(media_link) + break + except: + logging.info('KeyError: "href"\n' + 'Missing "href" attribute for {}' + .format(url)) + logging.info('Continue scanning for next ' + 'potential enclosure of {}' + .format(link)) + entry = { + "title": title, + "link": link, + "enclosure": media_link, + "entry_id": entry_id, + "date": date, + "read_status": read_status + } + new_entries.extend([entry]) + # await sqlite.add_entry( + # db_file, title, link, entry_id, + # url, date, read_status) + # await sqlite.set_date(db_file, url) + if len(new_entries): feed_id = await sqlite.get_feed_id(db_file, url) feed_id = feed_id[0] - await sqlite.update_feed_properties( - db_file, feed_id, len(feed["entries"]), updated) - # await update_feed_status - except ( - IncompleteReadError, - IncompleteRead, - error.URLError - ) as e: - logging.error(e) - return - # new_entry = 0 - for entry in entries: - if entry.has_key("published"): - date = entry.published - date = dt.rfc2822_to_iso8601(date) - elif entry.has_key("updated"): - date = entry.updated - date = dt.rfc2822_to_iso8601(date) - else: - date = dt.now() - if entry.has_key("link"): - # link = complete_url(source, entry.link) - link = join_url(url, entry.link) - link = trim_url(link) - else: - link = url - # title = feed["feed"]["title"] - # title = "{}: *{}*".format(feed["feed"]["title"], entry.title) - title = entry.title if entry.has_key("title") else date - entry_id = entry.id if entry.has_key("id") else link - feed_id = await sqlite.get_feed_id(db_file, url) - feed_id = feed_id[0] - exist = await sqlite.check_entry_exist( - db_file, feed_id, entry_id=entry_id, - title=title, link=link, date=date) - if not exist: - summary = entry.summary if entry.has_key("summary") else '' - read_status = 0 - pathname = urlsplit(link).path - string = ( - "{} {} {}" - ).format( - title, summary, pathname) - allow_list = await config.is_include_keyword( - db_file, "allow", string) - if not allow_list: - reject_list = await config.is_include_keyword( - db_file, "deny", string) - if reject_list: - read_status = 1 - logging.debug( - "Rejected : {}\n" - "Keyword : {}".format( - link, reject_list)) - if isinstance(date, int): - logging.error('Variable "date" is int: {}' - .format(date)) - media_link = '' - if entry.has_key("links"): - for e_link in entry.links: - try: - # if (link.rel == "enclosure" and - # (link.type.startswith("audio/") or - # link.type.startswith("image/") or - # link.type.startswith("video/")) - # ): - media_type = e_link.type[:e_link.type.index("/")] - if e_link.has_key("rel"): - if (e_link.rel == "enclosure" and - media_type in ("audio", "image", "video")): - media_link = e_link.href - media_link = join_url(url, e_link.href) - media_link = trim_url(media_link) - break - except: - logging.info('KeyError: "href"\n' - 'Missing "href" attribute for {}' - .format(url)) - logging.info('Continue scanning for next ' - 'potential enclosure of {}' - .format(link)) - entry = { - "title": title, - "link": link, - "enclosure": media_link, - "entry_id": entry_id, - "date": date, - "read_status": read_status - } - new_entries.extend([entry]) - # await sqlite.add_entry( - # db_file, title, link, entry_id, - # url, date, read_status) - # await sqlite.set_date(db_file, url) - if len(new_entries): - feed_id = await sqlite.get_feed_id(db_file, url) - feed_id = feed_id[0] - await sqlite.add_entries_and_update_timestamp( - db_file, feed_id, new_entries) + await sqlite.add_entries_and_update_timestamp(db_file, feed_id, + new_entries) async def download_document(self, message, jid, jid_file, message_text, ix_url, @@ -1286,8 +1288,7 @@ async def download_document(self, message, jid, jid_file, message_text, ix_url, status_type = 'dnd' status_message = ('📃️ Procesing request to produce {} document...' .format(ext.upper())) - XmppPresence.send(self, jid, status_message, - status_type=status_type) + XmppPresence.send(self, jid, status_message, status_type=status_type) db_file = config.get_pathname_to_database(jid_file) cache_dir = config.get_default_cache_directory() if ix_url: @@ -1313,9 +1314,9 @@ async def download_document(self, message, jid, jid_file, message_text, ix_url, logging.info('Processed URL (replace hostname): {}' .format(url)) result = await fetch.http(url) - data = result[0] - code = result[1] - if data: + if not result['error']: + data = result['content'] + code = result['status_code'] title = get_document_title(data) title = title.strip().lower() for i in (' ', '-'): @@ -1332,8 +1333,7 @@ async def download_document(self, message, jid, jid_file, message_text, ix_url, 'Failed to export {}. Reason: {}' .format(url, ext.upper(), error)) else: - url = await XmppUpload.start(self, jid, - filename) + url = await XmppUpload.start(self, jid, filename) chat_type = await get_chat_type(self, jid) XmppMessage.send_oob(self, jid, url, chat_type) else: @@ -1416,8 +1416,8 @@ async def extract_image_from_feed(db_file, feed_id, url): feed_url = sqlite.get_feed_url(db_file, feed_id) feed_url = feed_url[0] result = await fetch.http(feed_url) - document = result[0] - if document: + if not result['error']: + document = result['content'] feed = parse(document) for entry in feed.entries: try: @@ -1434,8 +1434,8 @@ async def extract_image_from_feed(db_file, feed_id, url): async def extract_image_from_html(url): result = await fetch.http(url) - data = result[0] - if data: + if not result['error']: + data = result['content'] try: document = Document(data) content = document.summary() @@ -1723,4 +1723,4 @@ async def remove_nonexistent_entries_json(db_file, url, feed): else: await sqlite.archive_entry(db_file, ix) limit = config.get_setting_value(db_file, "archive") - await sqlite.maintain_archive(db_file, limit) \ No newline at end of file + await sqlite.maintain_archive(db_file, limit) diff --git a/slixfeed/assets/feeds.toml b/slixfeed/assets/feeds.toml new file mode 100644 index 0000000..0803b93 --- /dev/null +++ b/slixfeed/assets/feeds.toml @@ -0,0 +1,840 @@ +# This is a default set of featured feeds allocated by locality (i.e. language code). +# Copy this file to ~/.config/slixfeed/feeds.toml if you want to modify the list. + +# NOTE +# TODO Consider splitting into files feeds-cz.toml, feeds-de.toml, feeds-el.toml. + +[[feeds]] +lang = "cs-cz" +name = "Česká pirátská strana" +link = "https://www.pirati.cz/feeds/atom/" +tags = ["czech", "party", "pirate"] + +[[feeds]] +lang = "cs-cz" +name = "Paralelní Polis" +link = "https://www.paralelnipolis.cz/feed/" +tags = ["communication", "czech", "hackerspace", "hardware", "internet", "makers", "technology", "telecom"] + +[[feeds]] +lang = "de-at" +name = "Alle Neuigkeiten von jotwewe.de" +link = "http://www.jotwewe.de/alles_de.xml" +tags = ["jabber", "jabrss", "xmpp", "syndication"] + +[[feeds]] +lang = "de-at" +name = "Piratenpartei Österreichs" +link = "https://piratenpartei.at/feed/" +tags = ["austria", "party", "pirate"] + +[[feeds]] +lang = "de-ch" +name = "Chaos Computer Club Basel" +link = "https://ccc-basel.ch/feeds/all.atom.xml" +tags = ["ccc", "club", "computer", "switzerland"] + +[[feeds]] +lang = "de-ch" +name = "GNU/Linux.ch" +link = "https://gnulinux.ch/rss.xml" +tags = ["computer", "industry", "computer", "linux", "mobile", "pda", "switzerland"] + +[[feeds]] +lang = "de-ch" +name = "Piratenpartei Schweiz" +link = "https://www.piratenpartei.ch/feed/" +tags = ["party", "pirate", "switzerland"] + +[[feeds]] +lang = "de-de" +name = "0d - Zeroday" +link = "https://0x0d.de/feed/mp3/" +tags = ["computer", "germany", "podcast"] + +[[feeds]] +lang = "de-de" +name = "Berlin XMPP Meetup" +link = "https://mov.im/?feed/pubsub.movim.eu/berlin-xmpp-meetup" +tags = ["event", "germany", "xmpp"] + +[[feeds]] +lang = "de-de" +name = "CCC Event Blog" +link = "https://events.ccc.de/feed" +tags = ["ccc", "club", "event"] + +[[feeds]] +lang = "de-de" +name = "Chaos Computer Club: Updates" +link = "https://www.ccc.de/de/rss/updates.rdf" +tags = ["ccc", "news", "politics", "privacy", "surveillance", "technology", "germany"] + +[[feeds]] +lang = "de-de" +name = "classless Kulla" +link = "https://www.classless.org/feed/atom/" +tags = ["europe ,germany ,history ,literature ,war"] + +[[feeds]] +lang = "de-de" +name = "Digitalcourage" +link = "https://digitalcourage.de/rss.xml" +tags = ["culture", "digital", "germany"] + +[[feeds]] +lang = "de-de" +name = "FSFE Events" +link = "https://fsfe.org/events/events.de.rss" +tags = ["computer", "events", "germany", "internet", "technology"] + +[[feeds]] +lang = "de-de" +name = "FSFE News" +link = "https://fsfe.org/news/news.de.rss" +tags = ["computer", "events", "germany", "internet", "privacy", "technology"] + +[[feeds]] +lang = "de-de" +name = "Jabber.de" +link = "https://www.jabber.de/?feed=rss2" +tags = ["germany", "jabber", "telecom", "xmpp"] + +[[feeds]] +lang = "de-de" +name = "KI-Verband" +link = "https://ki-verband.de/feed/" +tags = ["ai", "germany", "machine", "model", "learning", "technology"] + +[[feeds]] +lang = "de-de" +name = "media.ccc.de" +link = "https://media.ccc.de/news.atom" +tags = ["ccc", "technology", "video"] + +[[feeds]] +lang = "de-de" +name = "pimux.de" +link = "https://pimux.de/blog.rss" +tags = ["germany", "jabber", "mastodon", "telecom", "xmpp"] + +[[feeds]] +lang = "de-de" +name = "Sovereign Tech Fund" +link = "https://www.sovereigntechfund.de/de/feed.rss" +tags = ["germany", "technology", "open source"] + +[[feeds]] +lang = "de-de" +name = "WinFuture News" +link = "https://static.winfuture.de/feeds/WinFuture-News-rss2.0.xml" +tags = ["computer", "germany", "technology", "germany"] + +[[feeds]] +lang = "el-gr" +name = "Ηρακλής Παπαθεοδώρου • heracl.es" +link = "https://heracl.es/el/feed.xml" +tags = ["computer", "electronics", "greece", "technology"] + +[[feeds]] +lang = "el-gr" +name = "Κόμμα Πειρατών Ελλάδας – Pirate party of Greece" +link = "https://www.pirateparty.gr/feed/" +tags = ["greece", "party", "pirate"] + +[[feeds]] +lang = "en-au" +name = "Pirate Party Australia" +link = "https://pirateparty.org.au/feed/podcast/" +tags = ["australia", "party", "pirate"] + +[[feeds]] +lang = "en-ca" +name = "CAFE - The Canadian Association for Free Expression" +link = "http://cafe.nfshost.com/?feed=rss2" +tags = ["canada", "freedom", "government", "immigration", "society", "speech"] + +[[feeds]] +lang = "en-ca" +name = "Christian Faith and Copyfreedom" +link = "https://singpolyma.net/2015/07/christian-faith-and-copyfreedom/feed/" +tags = ["christianity", "copy", "freedom", "religion", "software", "technology"] + +[[feeds]] +lang = "en-ca" +name = "blog.jmp.chat's blog" +link = "https://blog.jmp.chat/atom.xml" +tags = ["jmp", "service", "sms", "telecom", "xmpp"] + +[[feeds]] +lang = "en-ca" +name = "RantMedia Forum" +link = "https://smf.rantradio.com/index.php?type=atom;action=.xml" +tags = ["canada", "forum", "politics", "privacy", "surveillance"] + +[[feeds]] +lang = "en-ca" +name = "Singpolyma" +link = "https://singpolyma.net/feed/" +tags = ["code", "computer", "culture", "jmp", "people", "software", "technology", "xmpp"] + +[[feeds]] +lang = "en-gb" +name = "A sysadmin's (mis)adventures" +link = "https://blog.woodpeckersnest.space/feed/" +tags = ["computer", "internet", "people", "technology", "xmpp"] + +[[feeds]] +lang = "en-gb" +name = "media.ccc.de" +link = "https://media.ccc.de/news.atom" +tags = ["ccc", "technology", "video"] + +[[feeds]] +lang = "en-gb" +name = "Christof Meerwald" +link = "https://cmeerw.org/blog.atom" +tags = ["austria", "computer", "linux", "people", "xmpp"] + +[[feeds]] +lang = "en-gb" +name = "German AI Association" +link = "https://ki-verband.de/en/feed/" +tags = ["ai", "machine", "model", "learning", "technology"] + +[[feeds]] +lang = "en-gb" +name = "La Quadrature du Net" +link = "https://www.laquadrature.net/en/feed/" +tags = ["news", "politics", "privacy", "surveillance"] + +[[feeds]] +lang = "en-gb" +name = "Pimux XMPP News" +link = "https://pimux.de/blog.rss" +tags = ["germany", "jabber", "mastodon", "telecom", "xmpp"] + +[[feeds]] +lang = "en-gb" +name = "Pirate Party UK" +link = "https://pirateparty.org.uk/feed.xml" +tags = ["party", "pirate", "uk"] + +[[feeds]] +lang = "en-gb" +name = "RIAT Institute" +link = "https://riat.at/feed/" +tags = ["art", "economics", "education", "hardware", "research", "technology"] + +[[feeds]] +lang = "en-gb" +name = "Snikket Blog on Snikket Chat" +link = "https://snikket.org/blog/index.xml" +tags = ["chat", "jabber", "telecom", "xmpp"] + +[[feeds]] +lang = "en-gb" +name = "The Brexit Party" +link = "https://www.thebrexitparty.org/feed/" +tags = ["europe", "politics", "uk"] + +[[feeds]] +lang = "en-us" +name = "153 News - Videos Being Watched" +link = "https://153news.net/rss.php?mode=watching" +tags = ["news", "politics", "usa", "video"] + +[[feeds]] +lang = "en-us" +name = "AudioBook Bay (ABB)" +link = "https://audiobookbay.is/feed/atom/" +tags = ["audiobook", "torrent"] + +[[feeds]] +lang = "en-us" +name = "Bald & Beards" +link = "https://www.baldandbeards.com/feed/" +tags = ["lifestyle", "men"] + +[[feeds]] +lang = "en-us" +name = "BlackListed News" +link = "https://www.blacklistednews.com/rss.php" +tags = ["news", "politics", "usa", "world"] + +[[feeds]] +lang = "en-us" +name = "Canoe Mail" +link = "https://canoemail.net/rss.xml" +tags = ["email", "jabber", "telecom", "xmpp"] + +[[feeds]] +lang = "en-us" +name = "CODEPINK - Women for Peace" +link = "https://www.codepink.org/news.rss" +tags = ["activism", "peace", "war", "women"] + +[[feeds]] +lang = "en-us" +name = "Ctrl blog" +link = "https://feed.ctrl.blog/latest.atom" +tags = ["computer", "technology"] + +[[feeds]] +lang = "en-us" +name = "Disroot Blog" +link = "https://disroot.org/en/blog.atom" +tags = ["decentralization", "privacy"] + +[[feeds]] +lang = "en-us" +name = "F-Droid" +link = "https://f-droid.org/feed.xml" +tags = ["android", "pda", "privacy"] + +[[feeds]] +lang = "en-us" +name = "Fairphone" +link = "https://www.fairphone.com/en/feed/" +tags = ["pda", "privacy"] + +[[feeds]] +lang = "en-us" +name = "Fakeologist.com" +link = "https://fakeologist.com/feed/" +tags = ["news", "politics", "usa", "world"] + +[[feeds]] +lang = "en-us" +name = "Fakeologist Forums" +link = "https://fakeologist.com/forums2/app.php/feed/news" +tags = ["forum", "politics", "usa", "world"] + +[[feeds]] +lang = "en-us" +name = "FSFE Events" +link = "https://fsfe.org/events/events.en.rss" +tags = ["computer", "events", "internet", "technology"] + +[[feeds]] +lang = "en-us" +name = "FSFE News" +link = "https://fsfe.org/news/news.en.rss" +tags = ["computer", "events", "internet", "privacy", "technology"] + +[[feeds]] +lang = "en-us" +name = "Hacker Public Radio" +link = "https://hackerpublicradio.org/hpr_ogg_rss.php" +tags = ["computer", "internet", "podcast", "technology"] + +[[feeds]] +lang = "en-us" +name = "Hardy Fruit Tree Nursery" +link = "https://www.hardyfruittrees.ca/feed/" +tags = ["farming", "food", "gardening", "survival"] + +[[feeds]] +lang = "en-us" +name = "Ice Age Farmer" +link = "https://www.iceagefarmer.com/feed/" +tags = ["farming", "food", "gardening", "survival"] + +[[feeds]] +lang = "en-us" +name = "International Consortium of Investigative Journalists" +link = "https://www.icij.org/feed/" +tags = ["news", "politics", "usa", "world"] + +[[feeds]] +lang = "en-us" +name = "Jacob's Unnamed Blog" +link = "https://jacobwsmith.xyz/feed.xml" +tags = ["book", "community", "culture", "family", "finance", "lifestyle", "market", "usa"] + +[[feeds]] +lang = "en-us" +name = "Juicing for Health" +link = "https://juicing-for-health.com/feed" +tags = ["beverage", "drink", "recipe"] + +[[feeds]] +lang = "en-us" +name = "karson777" +link = "https://videos.danksquad.org/feeds/videos.xml?videoChannelId=4711" +tags = ["computer", "game", "internet", "linux", "software", "technology"] + +[[feeds]] +lang = "en-us" +name = "Larken Rose" +link = "http://larkenrose.com/?format=feed&type=atom" +tags = ["news", "politics", "usa"] + +[[feeds]] +lang = "en-us" +name = "LibriVox's New Releases" +link = "https://librivox.org/rss/latest_releases" +tags = ["audiobook,"] + +[[feeds]] +lang = "en-us" +name = "Massachusetts Pirate Party" +link = "https://masspirates.org/blog/feed/" +tags = ["massachusetts", "party", "pirate", "usa"] + +[[feeds]] +lang = "en-us" +name = "Mercola.com" +link = "https://articles.mercola.com/sites/articles/rss.aspx" +tags = ["health"] + +[[feeds]] +lang = "en-us" +name = "Monal IM" +link = "https://monal-im.org/index.xml" +tags = ["iphone", "xmpp"] + +[[feeds]] +lang = "en-us" +name = "Mom on a Mission" +link = "https://www.mom-on-a-mission.blog/all-posts?format=rss" +tags = ["family", "farming", "food", "gardening", "survival"] + +[[feeds]] +lang = "en-us" +name = "NLnet News" +link = "https://nlnet.nl/feed.atom" +tags = ["decentralization", "privacy"] + +[[feeds]] +lang = "en-us" +name = "nobulart" +link = "https://nobulart.com/feed/" +tags = ["news", "survival", "politics", "usa", "world"] + +[[feeds]] +lang = "en-us" +name = "Opt Out Podcast" +link = "https://optoutpod.com/index.xml" +tags = ["podcast", "politics", "privacy", "surveillance"] + +[[feeds]] +lang = "en-us" +name = "Phish.in Music Updates" +link = "https://phish.in/feeds/rss" +tags = ["music"] + +[[feeds]] +lang = "en-us" +name = "PINE64" +link = "https://www.pine64.org/feed/" +tags = ["pda", "privacy"] + +[[feeds]] +lang = "en-us" +name = "PineTalk Podcast" +link = "https://www.pine64.org/feed/opus/" +tags = ["pda", "podcast", "privacy"] + +[[feeds]] +lang = "en-us" +name = "Pirate Parties International" +link = "https://pp-international.net/feed/" +tags = ["party", "pirate", "international"] + +[[feeds]] +lang = "en-us" +name = "Planet Jabber" +link = "https://planet.jabber.org/atom.xml" +tags = ["xmpp"] + +[[feeds]] +lang = "en-us" +name = "Ploum.net" +link = "https://ploum.net/atom_en.xml" +tags = ["computer", "decentralization", "internet", "linux", "technology"] + +[[feeds]] +lang = "en-us" +name = "PrivacySavvy" +link = "https://privacysavvy.com/" +tags = ["privacy"] + +[[feeds]] +lang = "en-us" +name = "postmarketOS Podcast" +link = "https://cast.postmarketos.org/feed.rss" +tags = ["pda", "podcast", "privacy"] + +[[feeds]] +lang = "en-us" +name = "Project Gemini news" +link = "https://gemini.circumlunar.space/news/atom.xml" +tags = ["gemini", "internet"] + +[[feeds]] +lang = "en-us" +name = "PUNCH" +link = "https://punchdrink.com/feed/" +tags = ["beverage", "drink", "recipe"] + +[[feeds]] +lang = "en-us" +name = "Radio 3Fourteen" +link = "https://redice.tv/rss/radio-3fourteen" +tags = ["culture", "podcast", "politics", "radio", "usa"] + +[[feeds]] +lang = "en-us" +name = "Real Liberty Media" +link = "https://www.reallibertymedia.com/category/podcasts/feed/?redirect=no" +tags = ["culture", "news", "podcast", "politics", "privacy", "surveillance", "usa"] + +[[feeds]] +lang = "en-us" +name = "Reclaim The Net" +link = "https://reclaimthenet.org/feed" +tags = ["news", "politics", "privacy", "surveillance"] + +[[feeds]] +lang = "en-us" +name = "Red Ice News" +link = "https://redice.tv/rss/news" +tags = ["culture", "news", "politics", "usa"] + +[[feeds]] +lang = "en-us" +name = "Red Ice Radio" +link = "https://redice.tv/rss/red-ice-radio" +tags = ["culture", "podcast", "politics", "radio", "usa"] + +[[feeds]] +lang = "en-us" +name = "Red Ice TV" +link = "https://redice.tv/rss/red-ice-tv" +tags = ["culture", "podcast", "politics", "usa", "vodcast"] + +[[feeds]] +lang = "en-us" +name = "Redecentralize Blog" +link = "https://redecentralize.org/blog/feed.rss" +tags = ["podcast", "privacy", "surveillance", "vodcast"] + +[[feeds]] +lang = "en-us" +name = "Road Runners Club of America" +link = "https://www.rrca.org/feed/" +tags = ["jog", "road", "run", "sports", "usa"] + +[[feeds]] +lang = "en-us" +name = "Seymour Hersh" +link = "https://seymourhersh.substack.com/feed" +tags = ["news", "politics", "usa", "war"] + +[[feeds]] +lang = "en-us" +name = "Signs of the Times" +link = "https://www.sott.net/xml_engine/signs_rss" +tags = ["europe", "industry", "news", "politics", "usa", "war", "world"] + +[[feeds]] +lang = "en-us" +name = "Simplified Privacy" +link = "https://simplifiedprivacy.com/feed/" +tags = ["news", "privacy", "surveillance", "vodcast"] + +[[feeds]] +lang = "en-us" +name = "Software Freedom Conservancy News" +link = "https://sfconservancy.org/feeds/news/" +tags = ["culture", "free software", "freedom", "liberty", "open source", "software"] + +[[feeds]] +lang = "en-us" +name = "Software Freedom Podcast" +link = "http://fsfe.org/news/podcast.en.rss" +tags = ["computer", "podcast", "technology"] + +[[feeds]] +lang = "en-us" +name = "Stop Spraying Us!" +link = "http://www.stopsprayingus.com/feed/" +tags = ["activism", "geoengineering"] + +[[feeds]] +lang = "en-us" +name = "Sweet Home 3D Blog" +link = "http://www.sweethome3d.com/blog/rss.xml" +tags = ["3d", "architecture", "design", "game"] + +[[feeds]] +lang = "en-us" +name = "Take Back Our Tech" +link = "https://takebackourtech.org/rss/" +tags = ["internet", "privacy", "surveillance"] + +[[feeds]] +lang = "en-us" +name = "The Bald Brothers" +link = "https://thebaldbrothers.com/feed/" +tags = ["lifestyle", "men"] + +[[feeds]] +lang = "en-us" +name = "The 250kb Club" +link = "https://250kb.club/rss.xml" +tags = ["webring"] + +[[feeds]] +lang = "en-us" +name = "The Conscious Resistance Network" +link = "https://theconsciousresistance.com/feed/" +tags = ["culture", "government", "podcast", "politics", "privacy", "surveillance", "usa"] + +[[feeds]] +lang = "en-us" +name = "The Corbett Report" +link = "https://www.corbettreport.com/feed/" +tags = ["podcast", "politics", "privacy", "surveillance", "usa", "vodcast"] + +[[feeds]] +lang = "en-us" +name = "The Organic Prepper" +link = "https://www.theorganicprepper.com/feed/" +tags = ["farming", "food", "gardening", "survival"] + +[[feeds]] +lang = "en-us" +name = "The XMPP Blog on XMPP" +link = "https://xmpp.org/feeds/all.atom.xml" +tags = ["jabber", "telecom", "xmpp"] + +[[feeds]] +lang = "en-us" +name = "Truthstream Media" +link = "http://truthstreammedia.com/feed/" +tags = ["culture", "privacy", "surveillance", "usa", "vodcast"] + +[[feeds]] +lang = "en-us" +name = "United States Pirate Party" +link = "https://uspirates.org/feed/" +tags = ["party", "pirate", "usa"] + +[[feeds]] +lang = "en-us" +name = "Xonotic" +link = "https://xonotic.org/index.xml" +tags = ["3d", "game"] + +[[feeds]] +lang = "en-us" +name = "yaxim" +link = "https://yaxim.org/atom.xml" +tags = ["android", "germany", "jabber", "telecom", "xmpp"] + +[[feeds]] +lang = "es-es" +name = "Disroot Blog" +link = "https://disroot.org/es/blog.atom" +tags = ["decentralization", "privacy"] + +[[feeds]] +lang = "fr-fr" +name = "Agate Blue" +link = "https://agate.blue/feed.xml" +tags = ["computer", "music"] + +[[feeds]] +lang = "fr-fr" +name = "Archlinux.fr [Forums]" +link = "https://forums.archlinux.fr/app.php/feed" +tags = ["forum", "linux"] + +[[feeds]] +lang = "fr-fr" +name = "Cours et Tutoriels sur le Langage SQL" +link = "https://sql.sh/feed" +tags = ["sql", "tutorial"] + +[[feeds]] +lang = "fr-fr" +name = "Developpez" +link = "https://www.developpez.com/index/atom" +tags = ["technology"] + +[[feeds]] +lang = "fr-fr" +name = "Disroot Blog" +link = "https://disroot.org/fr/blog.atom" +tags = ["decentralization", "privacy"] + +[[feeds]] +lang = "fr-fr" +name = "Framablog" +link = "https://framablog.org/feed/" +tags = ["fediverse", "framasoft", "open source", "peertube", "privacy", "software", "xmpp"] + +[[feeds]] +lang = "fr-fr" +name = "FSFE Events" +link = "https://fsfe.org/events/events.fr.rss" +tags = ["computer", "events", "internet", "technology"] + +[[feeds]] +lang = "fr-fr" +name = "FSFE News" +link = "https://fsfe.org/news/news.fr.rss" +tags = ["computer", "events", "internet", "privacy", "technology"] + +[[feeds]] +lang = "fr-fr" +name = "La Quadrature du Net" +link = "https://www.laquadrature.net/feed/" +tags = ["news", "politics", "privacy", "surveillance"] + +[[feeds]] +lang = "fr-fr" +name = "Ploum.net" +link = "https://ploum.net/atom_fr.xml" +tags = ["computer", "decentralization", "internet", "linux", "technology"] + +[[feeds]] +lang = "fr-fr" +name = "Wiki de sebsauvage.net" +link = "https://sebsauvage.net/wiki/feed.php" +tags = ["computer", "network"] + +[[feeds]] +lang = "he-il" +name = "ALIVE528" +link = "https://alive528.com/feed/" +tags = ["health", "news", "politics", "privacy", "surveillance"] + +[[feeds]] +lang = "he-il" +name = "אגוגו מגזין בריאות" +link = "https://www.agogo.co.il/feed/" +tags = ["food", "health"] + +[[feeds]] +lang = "he-il" +name = "האייל הקורא" +link = "http://www.haayal.co.il/xml/rss" +tags = ["news", "politics"] + +[[feeds]] +lang = "he-il" +name = "העין השביעית" +link = "https://www.the7eye.org.il/feed" +tags = ["industry", "media", "news", "propaganda"] + +[[feeds]] +lang = "he-il" +name = "מזבלה" +link = "https://mizbala.com/feed" +tags = ["industry", "media", "news", "propaganda"] + +[[feeds]] +lang = "he-il" +name = "פרויקט אמת אחרת" +link = "https://www.emetaheret.org.il/feed/" +tags = ["food", "health", "media", "news", "politics"] + +[[feeds]] +lang = "he-il" +name = "שיחה מקומית" +link = "https://www.mekomit.co.il/feed/" +tags = ["news", "politics"] + +[[feeds]] +lang = "it-it" +name = "Diggita / Prima Pagina" +link = "https://diggita.com/rss.php" +tags = ["computer", "culture", "food", "technology"] + +[[feeds]] +lang = "it-it" +name = "Feddit.it" +link = "https://feddit.it/feeds/local.xml?sort=Active" +tags = ["fediverse", "forum"] + +[[feeds]] +lang = "it-it" +name = "A sysadmin's (mis)adventures" +link = "https://blog.woodpeckersnest.space/feed/" +tags = ["computer", "internet", "people", "technology", "xmpp"] + +[[feeds]] +lang = "it-it" +name = "Avvocato a Roma" +link = "https://www.studiosabatino.it/feed/" +tags = ["law"] + +[[feeds]] +lang = "it-it" +name = "Disroot Blog" +link = "https://disroot.org/it/blog.atom" +tags = ["decentralization", "privacy"] + +[[feeds]] +lang = "it-it" +name = "LinuxTrent" +link = "https://www.linuxtrent.it/feed/" +tags = ["computer", "internet", "linux", "technology", "xmpp"] + +[[feeds]] +lang = "it-it" +name = "Mario Sabatino Gemini capsule" +link = "https://gemini.sabatino.cloud/rss.xml" +tags = ["computer", "people", "xmpp"] + +[[feeds]] +lang = "it-it" +name = "XMPP-IT Italian Community" +link = "https://www.xmpp-it.net/feed/" +tags = ["xmpp"] + +[[feeds]] +lang = "ja-jp" +name = "ニュース速報(総合)" +link = "https://mainichi.jp/rss/etc/mainichi-flash.rss" +tags = ["japan", "news", "politics", "world"] + +[[feeds]] +lang = "nl-nl" +name = "Piratenpartij" +link = "https://piratenpartij.nl/feed/" +tags = ["netherlands", "party", "pirate"] + +[[feeds]] +lang = "pl-pl" +name = "Fundacja Internet. Czas działać!" +link = "https://www.internet-czas-dzialac.pl/rss/" +tags = ["computer", "technology", "design"] + +[[feeds]] +lang = "ru-ru" +name = "Disroot Blog" +link = "https://disroot.org/ru/blog.atom" +tags = ["decentralization", "privacy"] + +[[feeds]] +lang = "ru-ru" +name = "За вашу и нашу Свободу!" +link = "https://lev-sharansky2.livejournal.com/data/atom" +tags = ["culture"] + +[[feeds]] +lang = "sv-se" +name = "Piratpartiet" +link = "https://piratpartiet.se/feed/" +tags = ["party", "pirate", "sweden"] + +[[feeds]] +lang = "vi-vn" +name = "Trần H. Trung" +link = "https://trung.fun/atom.xml" +tags = ["artist", "computer", "filmmaker", "people", "technology", "vietnam", "xmpp"] + diff --git a/slixfeed/config.py b/slixfeed/config.py index 41b09c7..55a7583 100644 --- a/slixfeed/config.py +++ b/slixfeed/config.py @@ -35,8 +35,11 @@ import tomli_w import tomllib def get_setting_value(db_file, key): - value = (sqlite.get_setting_value(db_file, key)[0] or - get_value("settings", "Settings", key)) + value = sqlite.get_setting_value(db_file, key) + if value: + value = value[0] + else: + value = get_value("settings", "Settings", key) try: value = int(value) except ValueError as e: @@ -448,7 +451,7 @@ async def remove_from_list(newwords, keywords): return val -async def is_include_keyword(db_file, key, string): +def is_include_keyword(db_file, key, string): """ Check keyword match. @@ -468,7 +471,8 @@ async def is_include_keyword(db_file, key, string): """ # async def reject(db_file, string): # async def is_blacklisted(db_file, string): - keywords = sqlite.get_filter_value(db_file, key) or '' + keywords = sqlite.get_filter_value(db_file, key) + keywords = keywords[0] if keywords else '' keywords = keywords.split(",") keywords = keywords + (open_config_file("lists.toml")[key]) for keyword in keywords: diff --git a/slixfeed/crawl.py b/slixfeed/crawl.py index 79691b0..8eae143 100644 --- a/slixfeed/crawl.py +++ b/slixfeed/crawl.py @@ -108,7 +108,7 @@ from urllib.parse import urlsplit, urlunsplit # return await callback(url) -async def probe_page(url, document): +async def probe_page(url, document=None): """ Parameters ---------- @@ -122,32 +122,34 @@ async def probe_page(url, document): result : list or str Single URL as list or selection of URLs as str. """ - result = None + if not document: + response = await fetch.http(url) + if not response['error']: + document = response['content'] try: # tree = etree.fromstring(res[0]) # etree is for xml tree = html.fromstring(document) + result = None except: - result = ( - "> {}\nFailed to parse URL as feed." - ).format(url) + logging.debug("Failed to parse URL as feed for {}.".format(url)) + result = {'link' : None, + 'index' : None, + 'name' : None, + 'code' : None, + 'error' : True, + 'exist' : None} if not result: - logging.debug( - "Feed auto-discovery engaged for {}".format(url)) + logging.debug("Feed auto-discovery engaged for {}".format(url)) result = await feed_mode_auto_discovery(url, tree) if not result: - logging.debug( - "Feed link scan mode engaged for {}".format(url)) + logging.debug("Feed link scan mode engaged for {}".format(url)) result = await feed_mode_scan(url, tree) if not result: - logging.debug( - "Feed arbitrary mode engaged for {}".format(url)) + logging.debug("Feed arbitrary mode engaged for {}".format(url)) result = await feed_mode_guess(url, tree) if not result: - logging.debug( - "No feeds were found for {}".format(url)) - result = ( - "> {}\nNo news feeds were found for URL." - ).format(url) + logging.debug("No feeds were found for {}".format(url)) + result = None return result @@ -182,8 +184,9 @@ async def feed_mode_guess(url, tree): ) if '.rss' not in paths else -1 # if paths.index('.rss'): # paths.extend([".atom", ".feed", ".rdf", ".rss"]) + parted_url_path = parted_url.path if parted_url.path else '/' for path in paths: - address = join_url(url, parted_url.path.split('/')[1] + path) + address = join_url(url, parted_url_path.split('/')[1] + path) if address not in urls: urls.extend([address]) # breakpoint() @@ -299,13 +302,15 @@ async def feed_mode_auto_discovery(url, tree): async def process_feed_selection(url, urls): feeds = {} for i in urls: - res = await fetch.http(i) - status_code = res[1] - if status_code == 200: - try: - feeds[i] = [parse(res[0])] - except: - continue + result = await fetch.http(i) + if not result['error']: + document = result['content'] + status_code = result['status_code'] + if status_code == 200: # NOTE This line might be redundant + try: + feeds[i] = [parse(document)] + except: + continue message = ( "Web feeds found for {}\n\n```\n" ).format(url) @@ -337,7 +342,7 @@ async def process_feed_selection(url, urls): # URL has been fetched, so that the receiving # function will scan that single URL instead of # listing it as a message. - url = {'url' : feed_url, + url = {'link' : feed_url, 'index' : None, 'name' : feed_name, 'code' : status_code, diff --git a/slixfeed/fetch.py b/slixfeed/fetch.py index 9ccab4c..74ce760 100644 --- a/slixfeed/fetch.py +++ b/slixfeed/fetch.py @@ -106,6 +106,7 @@ def http_response(url): response = None return response + async def http(url): """ Download content of given URL. @@ -120,13 +121,10 @@ async def http(url): msg: list or str Document or error message. """ - user_agent = ( - config.get_value( - "settings", "Network", "user-agent") - ) or 'Slixfeed/0.1' + user_agent = (config.get_value("settings", "Network", "user-agent") + or 'Slixfeed/0.1') headers = {'User-Agent': user_agent} - proxy = (config.get_value( - "settings", "Network", "http_proxy")) or '' + proxy = (config.get_value("settings", "Network", "http_proxy") or '') timeout = ClientTimeout(total=10) async with ClientSession(headers=headers) as session: # async with ClientSession(trust_env=True) as session: @@ -136,36 +134,39 @@ async def http(url): timeout=timeout ) as response: status = response.status - if response.status == 200: + if status == 200: try: - doc = await response.text() - # print (response.content_type) - msg = [doc, status] + document = await response.text() + result = {'charset': response.charset, + 'content': document, + 'content_length': response.content_length, + 'content_type': response.content_type, + 'error': False, + 'message': None, + 'original_url': url, + 'status_code': status, + 'response_url': response.url} except: - # msg = [ - # False, - # ("The content of this document " - # "doesn't appear to be textual." - # ) - # ] - msg = [ - False, "Document is too large or is not textual." - ] + result = {'error': True, + 'message': 'Could not get document.', + 'original_url': url, + 'status_code': status, + 'response_url': response.url} else: - msg = [ - False, "HTTP Error: " + str(status) - ] + result = {'error': True, + 'message': 'HTTP Error:' + str(status), + 'original_url': url, + 'status_code': status, + 'response_url': response.url} except ClientError as e: - # print('Error', str(e)) - msg = [ - False, "Error: " + str(e) - ] + result = {'error': True, + 'message': 'Error:' + str(e), + 'original_url': url} except TimeoutError as e: - # print('Timeout:', str(e)) - msg = [ - False, "Timeout: " + str(e) - ] - return msg + result = {'error': True, + 'message': 'Timeout:' + str(e), + 'original_url': url} + return result async def magnet(link): diff --git a/slixfeed/version.py b/slixfeed/version.py index 877b4ed..63e6a98 100644 --- a/slixfeed/version.py +++ b/slixfeed/version.py @@ -1,2 +1,2 @@ -__version__ = '0.1.10' -__version_info__ = (0, 1, 10) +__version__ = '0.1.11' +__version_info__ = (0, 1, 11) diff --git a/slixfeed/xmpp/client.py b/slixfeed/xmpp/client.py index 2411a76..46f9f3c 100644 --- a/slixfeed/xmpp/client.py +++ b/slixfeed/xmpp/client.py @@ -48,6 +48,8 @@ from slixmpp.plugins.xep_0048.stanza import Bookmarks import slixfeed.action as action import slixfeed.config as config +import slixfeed.crawl as crawl +import slixfeed.fetch as fetch from slixfeed.dt import timestamp import slixfeed.sqlite as sqlite from slixfeed.version import __version__ @@ -464,12 +466,15 @@ class Slixfeed(slixmpp.ClientXMPP): # ) # if jid == config.get_value('accounts', 'XMPP', 'operator'): - self['xep_0050'].add_command(node='subscription', - name='➕️ Add', - handler=self._handle_subscription_add) self['xep_0050'].add_command(node='subscriptions', name='📰️ Subscriptions', handler=self._handle_subscriptions) + self['xep_0050'].add_command(node='promoted', + name='🔮️ Featured', + handler=self._handle_promoted) + self['xep_0050'].add_command(node='subscription', + name='🔗️ Add', # 🪶️ + handler=self._handle_subscription_add) # self['xep_0050'].add_command(node='subscriptions_cat', # name='🔖️ Categories', # handler=self._handle_subscription) @@ -479,29 +484,32 @@ class Slixfeed(slixmpp.ClientXMPP): # self['xep_0050'].add_command(node='subscriptions_index', # name='📑️ Index (A - Z)', # handler=self._handle_subscription) - # TODO Join Filters and Settings into Preferences - self['xep_0050'].add_command(node='filters', - name='🛡️ Filters', - handler=self._handle_filters) + # TODO Join Filters, Schedule and Settings into Preferences self['xep_0050'].add_command(node='settings', name='📮️ Settings', handler=self._handle_settings) - if not self.is_component: # This will be changed with XEP-0222 XEP-0223 - self['xep_0050'].add_command(node='bookmarks', - name='📕 Bookmarks', - handler=self._handle_bookmarks) - self['xep_0050'].add_command(node='roster', - name='📓 Roster', # 📋 - handler=self._handle_roster) + self['xep_0050'].add_command(node='filters', + name='🛡️ Filters', + handler=self._handle_filters) + self['xep_0050'].add_command(node='schedule', + name='📅 Schedule', + handler=self._handle_schedule) self['xep_0050'].add_command(node='help', name='📔️ Manual', handler=self._handle_help) self['xep_0050'].add_command(node='totd', - name='💡️ TOTD', + name='💡️ Tips', handler=self._handle_totd) - self['xep_0050'].add_command(node='fotd', - name='🗓️ FOTD', - handler=self._handle_fotd) + if not self.is_component: # This will be changed with XEP-0222 XEP-0223 + self['xep_0050'].add_command(node='subscribers', + name='🏡️ Subscribers', # 🎫 + handler=self._handle_subscribers) + self['xep_0050'].add_command(node='bookmarks', + name='📕 Bookmarks', + handler=self._handle_bookmarks) + self['xep_0050'].add_command(node='roster', + name='📓 Roster', # 📋 + handler=self._handle_contacts) self['xep_0050'].add_command(node='activity', name='📠️ Activity', handler=self._handle_activity) @@ -542,8 +550,8 @@ class Slixfeed(slixmpp.ClientXMPP): jid = session['from'].bare jid_file = jid db_file = config.get_pathname_to_database(jid_file) - form = self['xep_0004'].make_form('form', 'Filters') - form['instructions'] = '🛡️ Manage filters' # 🪄️ + form = self['xep_0004'].make_form('form', 'Filter editor') + form['instructions'] = '🛡️ Edit and manage filters' # 🪄️ value = sqlite.get_filter_value(db_file, 'allow') if value: value = str(value[0]) form.add_field(var='allow', @@ -582,7 +590,7 @@ class Slixfeed(slixmpp.ClientXMPP): form = payload jid = session['from'].bare - form = self['xep_0004'].make_form('result', 'Filters') + form = self['xep_0004'].make_form('result', 'Done') form['instructions'] = ('✅️ Filters have been updated') jid_file = jid db_file = config.get_pathname_to_database(jid_file) @@ -611,7 +619,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscription_add(self, iq, session): jid = session['from'].bare - form = self['xep_0004'].make_form('form', 'Add Subscriptions') + form = self['xep_0004'].make_form('form', 'Add Subscription') form['instructions'] = '📰️ Add a new subscription' options = form.add_field(var='subscription', # TODO Make it possible to add several subscriptions at once; @@ -643,7 +651,7 @@ class Slixfeed(slixmpp.ClientXMPP): result = await action.add_feed(db_file, url) if isinstance(result, list): results = result - form = self['xep_0004'].make_form('form', 'Subscriptions') + form = self['xep_0004'].make_form('form', 'Select subscription') form['instructions'] = ('🔍️ Discovered {} subscriptions for {}' .format(len(results), url)) options = form.add_field(var='subscriptions', @@ -652,7 +660,7 @@ class Slixfeed(slixmpp.ClientXMPP): desc=('Select a subscription to add.'), required=True) for result in results: - options.addOption(result['name'], result['url']) + options.addOption(result['name'], result['link']) session['payload'] = form session['next'] = self._handle_subscription_editor session['has_next'] = True @@ -660,45 +668,41 @@ class Slixfeed(slixmpp.ClientXMPP): # response = ('News source "{}" is already listed ' # 'in the subscription list at index ' # '{}.\n{}'.format(result['name'], result['index'], - # result['url'])) + # result['link'])) # session['notes'] = [['warn', response]] # Not supported by Gajim # session['notes'] = [['info', response]] - form = self['xep_0004'].make_form('result', 'Subscriptions') - form['instructions'] = ('⚠️ Feed "{}" already exist as index {}' - .format(result['name'], result['index'])) + form = self['xep_0004'].make_form('form', 'Edit subscription') + form['instructions'] = ('📰️ ' + result['name']) options = form.add_field(var='subscriptions', - ftype='text-single', - label=result['url'], - desc='Choose next to edit subscription.', - value=result['url']) - # FIXME payload value does not pass, only []. + ftype='list-single', + label='Edit sbscription #{}'.format(result['index']), + # desc='Click URL to edit subscription.', + value=result['link']) + options.addOption(result['name'], result['link']) session['payload'] = form session['next'] = self._handle_subscription_editor - session['has_next'] = True + # session['has_next'] = False elif result['error']: - response = ('Failed to load URL.' + response = ('Failed to load URL {}' '\n\n' 'Reason: {}' - '\n\n' - 'URL: {}' - .format(result['code'], url)) + .format(url, result['code'])) session['notes'] = [['error', response]] session['next'] = None else: # response = ('News source "{}" has been ' # 'added to subscription list.\n{}' - # .format(result['name'], result['url'])) + # .format(result['name'], result['link'])) # session['notes'] = [['info', response]] - form = self['xep_0004'].make_form('result', 'Subscriptions') + form = self['xep_0004'].make_form('result', 'Done') form['instructions'] = ('✅️ News source "{}" has been added to ' 'subscription list as index {}' .format(result['name'], result['index'])) options = form.add_field(var='subscriptions', ftype='text-single', - label=result['url'], + label=result['link'], desc='Choose next to edit subscription.', - value=result['url']) - # FIXME payload value does not pass, only []. + value=result['link']) session['payload'] = form session['next'] = self._handle_subscription_editor session['has_next'] = True @@ -758,8 +762,8 @@ class Slixfeed(slixmpp.ClientXMPP): # FIXME There are feeds that are missing (possibly because of sortings) async def _handle_subscription(self, iq, session): jid = session['from'].bare - form = self['xep_0004'].make_form('form', 'Subscriptions') - form['instructions'] = '📰️ Edit subscription' + form = self['xep_0004'].make_form('form', 'Subscription editor') + form['instructions'] = '📰️ Edit subscription preferences and properties' # form.addField(var='interval', # ftype='text-single', # label='Interval period') @@ -829,7 +833,11 @@ class Slixfeed(slixmpp.ClientXMPP): options.addOption('Delete {} subscriptions'.format(url_count), 'delete') options.addOption('Export {} subscriptions'.format(url_count), 'export') else: - url = urls[0] + if isinstance(urls, list): + url = urls[0] + # elif isinstance(urls, str): + else: + url = urls feed_id = await sqlite.get_feed_id(db_file, url) feed_id = feed_id[0] title = sqlite.get_feed_title(db_file, feed_id) @@ -1019,7 +1027,7 @@ class Slixfeed(slixmpp.ClientXMPP): count = await action.import_opml(db_file, url) try: int(count) - form = self['xep_0004'].make_form('result', 'Import') + form = self['xep_0004'].make_form('result', 'Done') form['instructions'] = ('✅️ Feeds have been imported') message = '{} feeds have been imported to {}.'.format(count, jid) form.add_field(var='message', @@ -1058,7 +1066,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_export_complete(self, payload, session): jid = session['from'].bare jid_file = jid.replace('/', '_') - form = self['xep_0004'].make_form('result', 'Export') + form = self['xep_0004'].make_form('result', 'Done') form['instructions'] = ('✅️ Feeds have been exported') exts = payload['values']['filetype'] for ext in exts: @@ -1082,12 +1090,51 @@ class Slixfeed(slixmpp.ClientXMPP): return session - async def _handle_fotd(self, iq, session): - text = ('Here we publish featured news feeds!') + async def _handle_schedule(self, iq, session): + text = ('Schedule') + text += '\n\n' + text += 'Set days and hours to receive news.' session['notes'] = [['info', text]] return session + # TODO Exclude feeds that are already in database or requester. + # TODO Attempt to look up for feeds of hostname of JID (i.e. scan + # jabber.de for feeds for julient@jabber.de) + async def _handle_promoted(self, iq, session): + url = action.pick_a_feed() + form = self['xep_0004'].make_form('form', 'Subscribe') + # NOTE Refresh button would be of use + form['instructions'] = '🔮️ Featured subscriptions' # 🎲️ + options = form.add_field(var='subscription', + ftype="list-single", + label=url['name'], + value=url['link']) + options.addOption(url['name'], url['link']) + jid = session['from'].bare + if '@' in jid: + hostname = jid.split('@')[1] + url = 'http://' + hostname + result = await crawl.probe_page(url) + if not result: + url = {'url' : url, + 'index' : None, + 'name' : None, + 'code' : None, + 'error' : True, + 'exist' : False} + elif isinstance(result, list): + for url in result: + if url['link']: options.addOption(url['name'], url['link']) + else: + url = result + # Automatically set priority to 5 (highest) + if url['link']: options.addOption(url['name'], url['link']) + session['payload'] = form + session['next'] = self._handle_subscription_new + return session + + async def _handle_motd(self, iq, session): # TODO add functionality to attach image. text = ('Here you can add groupchat rules,post schedule, tasks or ' @@ -1103,9 +1150,11 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_credit(self, iq, session): + wrjx = action.manual('information.toml', 'thanks') form = self['xep_0004'].make_form('result', 'Credits') form['instructions'] = "We are XMPP" - form.add_field(ftype="text-multi", value=action.manual('information.toml', 'thanks')) + form.add_field(ftype="text-multi", + value=wrjx) # Gajim displays all form['instructions'] on top # Psi ignore the latter form['instructions'] @@ -1153,6 +1202,188 @@ class Slixfeed(slixmpp.ClientXMPP): return session + async def _handle_subscribers(self, iq, session): + jid = session['from'].bare + if jid == config.get_value('accounts', 'XMPP', 'operator'): + form = self['xep_0004'].make_form('form', 'Subscribers') + form['instructions'] = '📖️ Organize subscribers' + options = form.add_field(var='jid', + ftype='list-single', + label='Contacts', + desc='Select a contact.', + required=True) + contacts = await XmppRoster.get(self) + for contact in contacts: + contact_name = contacts[contact]['name'] + contact_name = contact_name if contact_name else contact + options.addOption(contact_name, contact) + options = form.add_field(var='action', + ftype='list-single', + label='Action', + value='message') + options.addOption('Resend authorization To', 'to') + options.addOption('Request authorization From', 'from') + options.addOption('Send message', 'message') + options.addOption('Remove', 'remove') + form.add_field(var='message', + ftype='text-multi', + label='Message', + desc='Add a descriptive message.') + session['payload'] = form + session['next'] = self._handle_subscribers_complete + session['has_next'] = True + else: + logging.warning('An unauthorized attempt to access bookmarks has ' + 'been detected!\n' + 'Details:\n' + ' Jabber ID: {}\n' + ' Timestamp: {}\n' + .format(jid, timestamp())) + session['notes'] = [['warn', 'This resource is restricted.']] + return session + + + async def _handle_subscribers_complete(self, iq, session): + pass + + + async def _handle_contacts(self, iq, session): + jid = session['from'].bare + if jid == config.get_value('accounts', 'XMPP', 'operator'): + form = self['xep_0004'].make_form('form', 'Contacts') + form['instructions'] = '📖️ Organize contacts' + options = form.add_field(var='jid', + ftype='list-single', + label='Contact', + desc='Select a contact.', + required=True) + contacts = await XmppRoster.get(self) + for contact in contacts: + contact_name = contacts[contact]['name'] + contact_name = contact_name if contact_name else contact + options.addOption(contact_name, contact) + options = form.add_field(var='action', + ftype='list-single', + label='Action', + value='view') + options.addOption('Display', 'view') + options.addOption('Edit', 'edit') + session['payload'] = form + session['next'] = self._handle_contact_action + session['has_next'] = True + else: + logging.warning('An unauthorized attempt to access bookmarks has ' + 'been detected!\n' + 'Details:\n' + ' Jabber ID: {}\n' + ' Timestamp: {}\n' + .format(jid, timestamp())) + session['notes'] = [['warn', 'This resource is restricted.']] + return session + + + async def _handle_contact_action(self, payload, session): + jid = payload['values']['jid'] + form = self['xep_0004'].make_form('form', 'Contact editor') + session['allow_complete'] = True + match payload['values']['action']: + case 'edit': + form['instructions'] = '📖️ Edit contact' + roster = await XmppRoster.get(self) + properties = roster[jid] + form.add_field(var='name', + ftype='text-single', + label='Name', + value=properties['name']) + case 'view': + session['has_next'] = False + session['next'] = None + session['allow_complete'] = None + form = self['xep_0004'].make_form('form', 'Contact info') + form['instructions'] = '📖️ Contact details' + roster = await XmppRoster.get(self) + properties = roster[jid] + contact_name = properties['name'] + contact_name = contact_name if contact_name else jid + form.add_field(var='name', + ftype='text-single', + label='Name', + value=properties['name']) + form.add_field(var='from', + ftype='boolean', + label='From', + value=properties['from']) + form.add_field(var='to', + ftype='boolean', + label='To', + value=properties['to']) + form.add_field(var='pending_in', + ftype='boolean', + label='Pending in', + value=properties['pending_in']) + form.add_field(var='pending_out', + ftype='boolean', + label='Pending out', + value=properties['pending_out']) + form.add_field(var='whitelisted', + ftype='boolean', + label='Whitelisted', + value=properties['whitelisted']) + form.add_field(var='subscription', + ftype='fixed', + label='Subscription', + value=properties['subscription']) + session['payload'] = form + session['prev'] = self._handle_contacts + session['allow_prev'] = True + # session['next'] = None + # session['has_next'] = False + # session['allow_complete'] = True + return session + + + async def _handle_contacts_complete(self, payload, session): + pass + + + async def _handle_contacts_view(self, payload, session): + jid = payload['values']['jid'] + roster = await XmppRoster.get(self) + properties = roster[jid] + form = self['xep_0004'].make_form('result', 'Contact') + contact_name = properties['name'] + contact_name = contact_name if contact_name else jid + form['instructions'] = '📝️ Edit contact {}'.format(contact_name) + form.add_field(var='name', + ftype='boolean', + label='From', + value=properties['from']) + form.add_field(var='to', + ftype='boolean', + label='To', + value=properties['to']) + form.add_field(var='pending_in', + ftype='boolean', + label='Pending in', + value=properties['pending_in']) + form.add_field(var='pending_out', + ftype='boolean', + label='Pending out', + value=properties['pending_out']) + form.add_field(var='whitelisted', + ftype='boolean', + label='Whitelisted', + value=properties['whitelisted']) + form.add_field(var='subscription', + ftype='text-single', + label='Subscription', + value=properties['subscription']) + session['payload'] = form + session['next'] = self._handle_bookmarks_complete + session['has_next'] = True + return session + + async def _handle_bookmarks(self, iq, session): jid = session['from'].bare if jid == config.get_value('accounts', 'XMPP', 'operator'): @@ -1175,15 +1406,14 @@ class Slixfeed(slixmpp.ClientXMPP): ' Jabber ID: {}\n' ' Timestamp: {}\n' .format(jid, timestamp())) - session['notes'] = [['warn', 'You are not allowed to access this resource.']] + session['notes'] = [['warn', 'This resource is restricted.']] return session async def _handle_bookmarks_editor(self, payload, session): jid = payload['values']['bookmarks'] properties = await XmppBookmark.properties(self, jid) - jid = session['from'].bare - form = self['xep_0004'].make_form('form', 'Edit bookmark') + form = self['xep_0004'].make_form('form', 'Bookmark editor') form['instructions'] = '📝️ Edit bookmark {}'.format(properties['name']) jid = properties['jid'].split('@') room = jid[0] @@ -1248,7 +1478,7 @@ class Slixfeed(slixmpp.ClientXMPP): here to persist across handler callbacks. """ - form = self['xep_0004'].make_form('result', 'Bookmarks') + form = self['xep_0004'].make_form('result', 'Done') form['instructions'] = ('✅️ Bookmark has been saved') # In this case (as is typical), the payload is a form values = payload['values'] @@ -1280,7 +1510,7 @@ class Slixfeed(slixmpp.ClientXMPP): jid = session['from'].bare jid_file = jid db_file = config.get_pathname_to_database(jid_file) - form = self['xep_0004'].make_form('form', 'Settings') + form = self['xep_0004'].make_form('form', 'Setting editor') form['instructions'] = ('📮️ Customize news updates') value = config.get_setting_value(db_file, 'enabled') @@ -1395,7 +1625,7 @@ class Slixfeed(slixmpp.ClientXMPP): # form = payload jid = session['from'].bare - form = self['xep_0004'].make_form('form', 'Settings') + form = self['xep_0004'].make_form('form', 'Done') form['instructions'] = ('✅️ Settings have been saved') jid_file = jid diff --git a/slixfeed/xmpp/process.py b/slixfeed/xmpp/process.py index 32fef71..590454a 100644 --- a/slixfeed/xmpp/process.py +++ b/slixfeed/xmpp/process.py @@ -546,13 +546,13 @@ async def message(self, message): response += ("Title : {}\n" "Link : {}\n" "\n" - .format(result['name'], result['url'])) + .format(result['name'], result['link'])) response += ('```\nTotal of {} feeds.' .format(len(results))) elif result['exist']: response = ('> {}\nNews source "{}" is already ' 'listed in the subscription list at ' - 'index {}'.format(result['url'], + 'index {}'.format(result['link'], result['name'], result['index'])) elif result['error']: @@ -561,7 +561,7 @@ async def message(self, message): else: response = ('> {}\nNews source "{}" has been ' 'added to subscription list.' - .format(result['url'], result['name'])) + .format(result['link'], result['name'])) # task.clean_tasks_xmpp(self, jid, ['status']) await task.start_tasks_xmpp(self, jid, ['status']) # except: diff --git a/slixfeed/xmpp/roster.py b/slixfeed/xmpp/roster.py index d1b9b84..d88036b 100644 --- a/slixfeed/xmpp/roster.py +++ b/slixfeed/xmpp/roster.py @@ -12,6 +12,13 @@ TODO class XmppRoster: + + async def get(self): + await self.get_roster() + contacts = self.client_roster + return contacts + + async def add(self, jid): """ Add JID to roster.