Compare commits
No commits in common. "master" and "master" have entirely different histories.
45 changed files with 9622 additions and 2639 deletions
22
README.md
22
README.md
|
@ -20,15 +20,14 @@ Slixfeed is primarily designed for XMPP (aka Jabber), yet it is built to be exte
|
|||
|
||||
## Features
|
||||
|
||||
- **Visual interface** - Interactive interface for XMPP using Ad-Hoc Commands,
|
||||
- **Ease** - Slixfeed automatically scans (i.e. crawls) for syndication feeds of given URL.
|
||||
- **Encryption** - Messages are encrypted with the OMEMO standard.
|
||||
- **Export** - Download articles as ePUB, HTML, Markdown and PDF.
|
||||
- **Filtering** - Filter news items using lists of allow and deny.
|
||||
- **Multimedia** - Display audios pictures and videos inline.
|
||||
- **Privacy** - Redirect to alternative back-ends, such as Invidious, Librarian, Nitter, for increased privacy, productivity and security.
|
||||
- **Portable** - Export and import feeds with a standard OPML file.
|
||||
- **Simultaneous** - Slixfeed is designed to handle multiple contacts, including groupchats, Simultaneously.
|
||||
- **Visual interface** - Interactive interface for XMPP using Ad-Hoc Commands,
|
||||
|
||||
## Preview
|
||||
|
||||
|
@ -57,18 +56,7 @@ It is possible to install Slixfeed using pip and pipx.
|
|||
```
|
||||
$ python3 -m venv .venv
|
||||
$ source .venv/bin/activate
|
||||
```
|
||||
|
||||
##### Install
|
||||
|
||||
```
|
||||
$ pip install git+https://git.xmpp-it.net/sch/Slixfeed
|
||||
```
|
||||
|
||||
##### Install (OMEMO)
|
||||
|
||||
```
|
||||
$ pip install git+https://git.xmpp-it.net/sch/Slixfeed[omemo]
|
||||
$ pip install git+https://gitgud.io/sjehuda/slixfeed
|
||||
```
|
||||
|
||||
#### pipx
|
||||
|
@ -76,14 +64,14 @@ $ pip install git+https://git.xmpp-it.net/sch/Slixfeed[omemo]
|
|||
##### Install
|
||||
|
||||
```
|
||||
$ pipx install git+https://git.xmpp-it.net/sch/Slixfeed
|
||||
$ pipx install git+https://gitgud.io/sjehuda/slixfeed
|
||||
```
|
||||
|
||||
##### Update
|
||||
|
||||
```
|
||||
$ pipx uninstall slixfeed
|
||||
$ pipx install git+https://git.xmpp-it.net/sch/Slixfeed
|
||||
$ pipx install git+https://gitgud.io/sjehuda/slixfeed
|
||||
```
|
||||
|
||||
### Start
|
||||
|
@ -99,7 +87,7 @@ $ slixfeed
|
|||
|
||||
## Recommended Clients
|
||||
|
||||
Slixfeed works with any XMPP chat client; if you want to make use of the visual interface Slixfeed has to offer (i.e. Ad-Hoc Commands), then you are advised to use [Cheogram](https://cheogram.com), [Converse](https://conversejs.org), [Gajim](https://gajim.org), [LeechCraft](https://leechcraft.org/plugins-azoth-xoox), [monocles chat](https://monocles.chat), [Movim](https://mov.im), [Poezio](https://poez.io), [Psi](https://psi-im.org) or [Psi+](https://psi-plus.com).
|
||||
Slixfeed works with any XMPP chat client; if you want to make use of the visual interface Slixfeed has to offer (i.e. Ad-Hoc Commands), then you are advised to use [Cheogram](https://cheogram.com), [Converse](https://conversejs.org), [Gajim](https://gajim.org), [monocles chat](https://monocles.chat), [Movim](https://mov.im), [Poezio](https://poez.io), [Psi](https://psi-im.org) or [Psi+](https://psi-plus.com).
|
||||
|
||||
### Support
|
||||
|
||||
|
|
|
@ -37,28 +37,27 @@ keywords = [
|
|||
"xml",
|
||||
"xmpp",
|
||||
]
|
||||
|
||||
# urls = {Homepage = "https://gitgud.io/sjehuda/slixfeed"}
|
||||
|
||||
dependencies = [
|
||||
"aiofiles",
|
||||
"aiohttp",
|
||||
# "daemonize",
|
||||
"feedparser",
|
||||
"lxml",
|
||||
# "pysocks",
|
||||
"python-dateutil",
|
||||
"requests",
|
||||
"slixmpp",
|
||||
"tomli", # Python 3.10
|
||||
"tomli_w",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://slixfeed.woodpeckersnest.space"
|
||||
Repository = "https://git.xmpp-it.net/sch/Slixfeed"
|
||||
Homepage = "http://slixfeed.i2p/"
|
||||
Repository = "https://gitgud.io/sjehuda/slixfeed"
|
||||
Issues = "https://gitgud.io/sjehuda/slixfeed/issues"
|
||||
|
||||
|
||||
[project.optional-dependencies]
|
||||
omemo = ["slixmpp-omemo"]
|
||||
proxy = ["pysocks"]
|
||||
|
||||
# [project.readme]
|
||||
|
|
|
@ -58,8 +58,6 @@ TODO
|
|||
# res = response (HTTP)
|
||||
from argparse import ArgumentParser
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
# from eliot import start_action, to_file
|
||||
|
@ -67,9 +65,8 @@ import sys
|
|||
# # with start_action(action_type='set_date()', jid=jid):
|
||||
# # with start_action(action_type='message()', msg=msg):
|
||||
|
||||
from slixfeed.config import Settings, Share, Cache
|
||||
import slixfeed.config as config
|
||||
from slixfeed.log import Logger
|
||||
from slixfeed.utilities import Toml
|
||||
from slixfeed.version import __version__
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
@ -81,44 +78,10 @@ logger = Logger(__name__)
|
|||
|
||||
def main():
|
||||
|
||||
directory = os.path.dirname(__file__)
|
||||
|
||||
# Copy data files
|
||||
directory_data = Share.get_directory()
|
||||
if not os.path.exists(directory_data):
|
||||
directory_assets = os.path.join(directory, 'assets')
|
||||
directory_assets_new = shutil.copytree(directory_assets, directory_data)
|
||||
print(f'Data directory {directory_assets_new} has been created and populated.')
|
||||
|
||||
# Copy settings files
|
||||
directory_settings = Settings.get_directory()
|
||||
if not os.path.exists(directory_settings):
|
||||
directory_configs = os.path.join(directory, 'configs')
|
||||
directory_settings_new = shutil.copytree(directory_configs, directory_settings)
|
||||
print(f'Settings directory {directory_settings_new} has been created and populated.')
|
||||
|
||||
# Create cache directories
|
||||
directory_cache = Cache.get_directory()
|
||||
if not os.path.exists(directory_cache):
|
||||
print(f'Creating a cache directory at {directory_cache}.')
|
||||
os.mkdir(directory_cache)
|
||||
for subdirectory in ('md', 'enclosure', 'markdown', 'opml', 'readability'):
|
||||
subdirectory_cache = os.path.join(directory_cache, subdirectory)
|
||||
if not os.path.exists(subdirectory_cache):
|
||||
print(f'Creating a cache subdirectory at {subdirectory_cache}.')
|
||||
os.mkdir(subdirectory_cache)
|
||||
|
||||
filename_settings = os.path.join(directory_settings, 'settings.toml')
|
||||
settings = Toml.open_file(filename_settings)
|
||||
network_settings = settings['network']
|
||||
|
||||
# Configure account
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
config_dir = config.get_default_config_directory()
|
||||
logger.info('Reading configuration from {}'.format(config_dir))
|
||||
print('Reading configuration from {}'.format(config_dir))
|
||||
network_settings = config.get_values('settings.toml', 'network')
|
||||
print('User agent:', network_settings['user_agent'] or 'Slixfeed/0.1')
|
||||
if network_settings['http_proxy']: print('HTTP Proxy:', network_settings['http_proxy'])
|
||||
|
||||
|
@ -160,6 +123,10 @@ def main():
|
|||
# Setup logging.
|
||||
logging.basicConfig(level=args.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
# # Setup logging.
|
||||
# logging.basicConfig(level=args.loglevel,
|
||||
# format='%(levelname)-8s %(message)s')
|
||||
# # logging.basicConfig(format='[%(levelname)s] %(message)s')
|
||||
# logger = logging.getLogger()
|
||||
# logdbg = logger.debug
|
||||
|
@ -197,33 +164,28 @@ def main():
|
|||
# if not alias:
|
||||
# alias = (input('Alias: ')) or 'Slixfeed'
|
||||
|
||||
filename_accounts = os.path.join(directory_settings, 'accounts.toml')
|
||||
accounts = Toml.open_file(filename_accounts)
|
||||
accounts_xmpp = accounts['xmpp']
|
||||
account_xmpp = config.get_values('accounts.toml', 'xmpp')
|
||||
|
||||
# Try configuration file
|
||||
if 'client' in accounts_xmpp:
|
||||
if 'client' in account_xmpp:
|
||||
from slixfeed.xmpp.client import XmppClient
|
||||
|
||||
accounts_xmpp_client = accounts_xmpp['client']
|
||||
jid = accounts_xmpp_client['jid']
|
||||
password = accounts_xmpp_client['password']
|
||||
alias = accounts_xmpp_client['alias'] if 'alias' in accounts_xmpp_client else None
|
||||
hostname = accounts_xmpp_client['hostname'] if 'hostname' in accounts_xmpp_client else None
|
||||
port = accounts_xmpp_client['port'] if 'port' in accounts_xmpp_client else None
|
||||
jid = account_xmpp['client']['jid']
|
||||
password = account_xmpp['client']['password']
|
||||
alias = account_xmpp['client']['alias'] if 'alias' in account_xmpp['client'] else None
|
||||
hostname = account_xmpp['client']['hostname'] if 'hostname' in account_xmpp['client'] else None
|
||||
port = account_xmpp['client']['port'] if 'port' in account_xmpp['client'] else None
|
||||
XmppClient(jid, password, hostname, port, alias)
|
||||
# xmpp_client = Slixfeed(jid, password, hostname, port, alias)
|
||||
# xmpp_client.connect((hostname, port)) if hostname and port else xmpp_client.connect()
|
||||
# xmpp_client.process()
|
||||
|
||||
if 'component' in accounts_xmpp:
|
||||
if 'component' in account_xmpp:
|
||||
from slixfeed.xmpp.component import XmppComponent
|
||||
accounts_xmpp_component = accounts_xmpp['component']
|
||||
jid = accounts_xmpp_component['jid']
|
||||
secret = accounts_xmpp_component['password']
|
||||
alias = accounts_xmpp_component['alias'] if 'alias' in accounts_xmpp_component else None
|
||||
hostname = accounts_xmpp_component['hostname'] if 'hostname' in accounts_xmpp_component else None
|
||||
port = accounts_xmpp_component['port'] if 'port' in accounts_xmpp_component else None
|
||||
jid = account_xmpp['component']['jid']
|
||||
secret = account_xmpp['component']['password']
|
||||
alias = account_xmpp['component']['alias'] if 'alias' in account_xmpp['component'] else None
|
||||
hostname = account_xmpp['component']['hostname'] if 'hostname' in account_xmpp['component'] else None
|
||||
port = account_xmpp['component']['port'] if 'port' in account_xmpp['component'] else None
|
||||
XmppComponent(jid, secret, hostname, port, alias)
|
||||
# xmpp_component = SlixfeedComponent(jid, secret, hostname, port, alias)
|
||||
# xmpp_component.connect()
|
||||
|
|
0
slixfeed/assets/__init__.py
Normal file
0
slixfeed/assets/__init__.py
Normal file
|
@ -28,9 +28,9 @@ Good luck!
|
|||
|
||||
filetypes = "Atom, JSON, RDF, RSS, XML."
|
||||
platforms = "XMPP"
|
||||
# platforms = "ActivityPub, BitMessage, Briar, DeltaChat, Email, IRC, LXMF, MQTT, Nostr, Session, Tox."
|
||||
# platforms = "ActivityPub, Briar, DeltaChat, Email, IRC, LXMF, MQTT, Nostr, Session, Tox."
|
||||
comment = "For ideal experience, we recommend using XMPP." # Nostr, Session or DeltaChat
|
||||
url = "https://git.xmpp-it.net/sch/Slixfeed"
|
||||
url = "https://gitgud.io/sjehuda/slixfeed"
|
||||
|
||||
[[about]]
|
||||
name = "slixmpp"
|
||||
|
@ -245,7 +245,7 @@ and webhooks.
|
|||
User ⇄ XMPP client ⇄ XMPP Server ⇄ XMPP Bot ⇄ REST API
|
||||
"""]
|
||||
interface = "Groupchat"
|
||||
url = "https://git.xmpp-it.net/roughnecks/xmpp-bot"
|
||||
url = "https://github.com/nioc/xmpp-bot"
|
||||
|
||||
[[legal]]
|
||||
title = "Legal"
|
||||
|
@ -260,7 +260,7 @@ Slixfeed is distributed in the hope that it will be useful, but WITHOUT ANY \
|
|||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR \
|
||||
A PARTICULAR PURPOSE. See the MIT License for more details.
|
||||
"""]
|
||||
link = "https://git.xmpp-it.net/sch/Slixfeed"
|
||||
link = "https://gitgud.io/sjehuda/slixfeed"
|
||||
|
||||
[[license]]
|
||||
title = "License"
|
||||
|
@ -653,15 +653,10 @@ or on your desktop.
|
|||
url = "https://conversejs.org"
|
||||
platform = "HTML (Web)"
|
||||
|
||||
[[clients]]
|
||||
name = "Gajim"
|
||||
info = "XMPP client for desktop"
|
||||
info = ["""
|
||||
Gajim aims to be an easy to use and fully-featured XMPP client. \
|
||||
It is open source and released under the GNU General Public License (GPL).
|
||||
"""]
|
||||
url = "https://gajim.org"
|
||||
platform = "Any"
|
||||
# [[clients]]
|
||||
# name = "Gajim"
|
||||
# info = "XMPP client for desktop"
|
||||
# url = "https://gajim.org"
|
||||
|
||||
# [[clients]]
|
||||
# name = "Monal IM"
|
|
@ -213,18 +213,6 @@ read <url> <index>
|
|||
Display specified entry number from given <url> by given <index>.
|
||||
"""
|
||||
|
||||
[pubsub]
|
||||
pubsub = """
|
||||
pubsub [off|on]
|
||||
Designate JID as PubSub service.
|
||||
"""
|
||||
|
||||
[send]
|
||||
send = """
|
||||
send <pubsub>/<jid> <url> <node>
|
||||
Send feeds to given JID.
|
||||
"""
|
||||
|
||||
[search]
|
||||
feeds = """
|
||||
feeds
|
||||
|
@ -248,24 +236,6 @@ stats = """
|
|||
stats
|
||||
Show general statistics.
|
||||
"""
|
||||
|
||||
[list]
|
||||
blacklist = """
|
||||
blacklist
|
||||
List all blacklisted JIDs.
|
||||
"""
|
||||
blacklist_action = """
|
||||
blacklist [add|delete] <jid>
|
||||
Manage allowed list.
|
||||
"""
|
||||
whitelist = """
|
||||
whitelist
|
||||
List all whitelisted JIDs.
|
||||
"""
|
||||
whitelist_action = """
|
||||
whitelist [add|delete] <jid>
|
||||
Manage denied list.
|
||||
"""
|
||||
# analyses = """
|
||||
# analyses
|
||||
# Show report and statistics of feeds.
|
2108
slixfeed/assets/feeds.csv
Normal file
2108
slixfeed/assets/feeds.csv
Normal file
File diff suppressed because it is too large
Load diff
|
@ -66,7 +66,7 @@ tags = ["event", "germany", "xmpp"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "de-de"
|
||||
name = "journal | hasecke"
|
||||
name = "blog | hasecke"
|
||||
link = "https://www.hasecke.eu/index.xml"
|
||||
tags = ["linux", "p2p", "software", "technology"]
|
||||
|
||||
|
@ -78,7 +78,7 @@ tags = ["computer", "industry", "electronics", "technology"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "de-de"
|
||||
name = "CCC Event Journal"
|
||||
name = "CCC Event Blog"
|
||||
link = "https://events.ccc.de/feed"
|
||||
tags = ["ccc", "club", "event"]
|
||||
|
||||
|
@ -188,13 +188,13 @@ tags = ["linux", "debian", "ubuntu", "industry"]
|
|||
lang = "en"
|
||||
name = "Dig Deeper"
|
||||
link = "https://diggy.club/atom.xml"
|
||||
tags = ["linux", "health", "computer", "wisdom", "research", "life", "industry"]
|
||||
tags = ["linux", "health", "computer", "wisdom", "life", "industry"]
|
||||
|
||||
[[feeds]]
|
||||
lang = "en"
|
||||
name = "Earth Newspaper"
|
||||
link = "https://earthnewspaper.com/feed/atom/"
|
||||
tags = ["technology", "sports", "culture", "world", "war", "politics"]
|
||||
tags = ["technology", "world", "war", "politics"]
|
||||
|
||||
[[feeds]]
|
||||
lang = "en"
|
||||
|
@ -208,12 +208,6 @@ name = "her.st - Do you see it yet?"
|
|||
link = "https://her.st/feed.xml"
|
||||
tags = ["lifestyle", "technology", "xmpp", "computer", "code", "llm", "syndication", "minimalism", "linux", "self-hosting", ".net", "go", "python", "philosophy", "psychology", "privacy", "security"]
|
||||
|
||||
[[feeds]]
|
||||
lang = "en"
|
||||
name = "Hippo (Badri Sunderarajan)"
|
||||
link = "https://badrihippo.thekambattu.rocks/feed.xml"
|
||||
tags = ["computer", "mobian", "gerda", "pris", "prav", "kaios", "linux", "phosh", "browser", "telecommunication", "internet", "xmpp"]
|
||||
|
||||
[[feeds]]
|
||||
lang = "en"
|
||||
name = "Lagrange Gemini Client"
|
||||
|
@ -222,19 +216,13 @@ tags = ["gemini", "gopher", "browser", "telecommunication", "internet"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "en"
|
||||
name = "[ngn.tf] | journal"
|
||||
name = "[ngn.tf] | blog"
|
||||
link = "https://api.ngn.tf/blog/feed.atom"
|
||||
tags = ["computer", "service", "technology", "telecommunication", "xmpp"]
|
||||
|
||||
[[feeds]]
|
||||
lang = "en"
|
||||
name = "Proycon's Journal"
|
||||
link = "https://proycon.anaproy.nl/rss.xml"
|
||||
tags = ["computer", "technology", "telecommunication", "postmarketos", "music", "piano", "privacy"]
|
||||
|
||||
[[feeds]]
|
||||
lang = "en"
|
||||
name = "RTP - Right To Privacy Journal"
|
||||
name = "RTP Blog"
|
||||
link = "http://righttoprivacy.i2p/rss/"
|
||||
tags = ["computer", "service", "technology", "telecommunication", "i2p", "privacy"]
|
||||
|
||||
|
@ -282,7 +270,7 @@ tags = ["christianity", "copy", "freedom", "religion", "software", "technology"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "en-ca"
|
||||
name = "JMP's Journal"
|
||||
name = "blog.jmp.chat's blog"
|
||||
link = "https://blog.jmp.chat/atom.xml"
|
||||
tags = ["jmp", "service", "sms", "telecommunication", "xmpp"]
|
||||
|
||||
|
@ -330,7 +318,7 @@ tags = ["news", "politics", "privacy", "surveillance"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "en-gb"
|
||||
name = "op-co.de journal"
|
||||
name = "op-co.de blog"
|
||||
link = "https://op-co.de/blog/index.rss"
|
||||
tags = ["code", "germany", "jabber", "mastodon", "telecommunication", "xmpp"]
|
||||
|
||||
|
@ -360,7 +348,7 @@ tags = ["art", "economics", "education", "hardware", "research", "technology"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "en-gb"
|
||||
name = "Snikket Journal on Snikket Chat"
|
||||
name = "Snikket Blog on Snikket Chat"
|
||||
link = "https://snikket.org/blog/index.xml"
|
||||
tags = ["chat", "jabber", "telecommunication", "xmpp"]
|
||||
|
||||
|
@ -380,7 +368,7 @@ tags = ["design", "diy", "household"]
|
|||
lang = "en-us"
|
||||
name = "12bytes.org"
|
||||
link = "https://12bytes.org/feed.xml"
|
||||
tags = ["conspiracy", "linux", "computer", "security", "privacy", "culture", "health", "government", "war", "world"]
|
||||
tags = ["conspiracy", "health", "government", "war", "world"]
|
||||
|
||||
[[feeds]]
|
||||
lang = "en-us"
|
||||
|
@ -418,6 +406,12 @@ 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 = "Delta Chat - Messenger based on e-mail"
|
||||
|
@ -426,16 +420,10 @@ tags = ["email", "telecommunication"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "en-us"
|
||||
name = "Disroot Journal"
|
||||
name = "Disroot Blog"
|
||||
link = "https://disroot.org/en/blog.atom"
|
||||
tags = ["decentralization", "privacy"]
|
||||
|
||||
[[feeds]]
|
||||
lang = "en-us"
|
||||
name = "Earthing Institute"
|
||||
link = "https://earthinginstitute.net/feed/atom/"
|
||||
tags = ["health", "meditation", "yoga"]
|
||||
|
||||
[[feeds]]
|
||||
lang = "en-us"
|
||||
name = "F-Droid"
|
||||
|
@ -498,7 +486,7 @@ tags = ["news", "politics", "usa", "world"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "en-us"
|
||||
name = "Jacob's Unnamed Journal"
|
||||
name = "Jacob's Unnamed Blog"
|
||||
link = "https://jacobwsmith.xyz/feed.xml"
|
||||
tags = ["book", "community", "culture", "family", "finance", "lifestyle", "market", "usa"]
|
||||
|
||||
|
@ -636,7 +624,7 @@ tags = ["gemini", "internet"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "en-us"
|
||||
name = "Public Intelligence Journal"
|
||||
name = "Public Intelligence Blog"
|
||||
link = "https://phibetaiota.net/feed/"
|
||||
tags = ["cia", "conspiracy", "health", "government", "war", "world"]
|
||||
|
||||
|
@ -684,7 +672,7 @@ tags = ["culture", "podcast", "politics", "usa", "vodcast"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "en-us"
|
||||
name = "Redecentralize Journal"
|
||||
name = "Redecentralize Blog"
|
||||
link = "https://redecentralize.org/blog/feed.rss"
|
||||
tags = ["podcast", "privacy", "surveillance", "vodcast"]
|
||||
|
||||
|
@ -732,7 +720,7 @@ tags = ["activism", "geoengineering"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "en-us"
|
||||
name = "Sweet Home 3D Journal"
|
||||
name = "Sweet Home 3D Blog"
|
||||
link = "http://www.sweethome3d.com/blog/rss.xml"
|
||||
tags = ["3d", "architecture", "design", "game"]
|
||||
|
||||
|
@ -780,7 +768,7 @@ tags = ["farming", "food", "gardening", "survival"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "en-us"
|
||||
name = "The XMPP Journal on XMPP"
|
||||
name = "The XMPP Blog on XMPP"
|
||||
link = "https://xmpp.org/feeds/all.atom.xml"
|
||||
tags = ["jabber", "telecommunication", "xmpp"]
|
||||
|
||||
|
@ -828,7 +816,7 @@ tags = ["decentralization", "development", "electronics", "networking", "privacy
|
|||
|
||||
[[feeds]]
|
||||
lang = "es-es"
|
||||
name = "Disroot Journal"
|
||||
name = "Disroot Blog"
|
||||
link = "https://disroot.org/es/blog.atom"
|
||||
tags = ["decentralization", "privacy"]
|
||||
|
||||
|
@ -858,13 +846,13 @@ tags = ["technology"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "fr-fr"
|
||||
name = "Disroot Journal"
|
||||
name = "Disroot Blog"
|
||||
link = "https://disroot.org/fr/blog.atom"
|
||||
tags = ["decentralization", "privacy"]
|
||||
|
||||
[[feeds]]
|
||||
lang = "fr-fr"
|
||||
name = "Frama Journal"
|
||||
name = "Framablog"
|
||||
link = "https://framablog.org/feed/"
|
||||
tags = ["fediverse", "framasoft", "open source", "peertube", "privacy", "software", "xmpp"]
|
||||
|
||||
|
@ -960,7 +948,7 @@ tags = ["computer", "culture", "food", "technology"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "it-it"
|
||||
name = "Disroot Journal"
|
||||
name = "Disroot Blog"
|
||||
link = "https://disroot.org/it/blog.atom"
|
||||
tags = ["decentralization", "privacy"]
|
||||
|
||||
|
@ -1014,7 +1002,7 @@ tags = ["computer", "technology", "design"]
|
|||
|
||||
[[feeds]]
|
||||
lang = "ru-ru"
|
||||
name = "Disroot Journal"
|
||||
name = "Disroot Blog"
|
||||
link = "https://disroot.org/ru/blog.atom"
|
||||
tags = ["decentralization", "privacy"]
|
||||
|
|
@ -1,48 +1 @@
|
|||
<svg height="600" width="600" xmlns="http://www.w3.org/2000/svg" xml:space="preserve">
|
||||
<defs>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="5" />
|
||||
<feOffset dx="5" dy="5" result="offsetblur" />
|
||||
<feFlood flood-color="rgba(0,0,0,0.5)" />
|
||||
<feComposite in2="offsetblur" operator="in" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<!-- Glass Gradient -->
|
||||
<linearGradient id="glassGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:rgba(255, 255, 255, 0.3); stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:rgba(255, 255, 255, 0.1); stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Black shapes with orange margins -->
|
||||
<path d="M167 406a60 60 0 1 1-120 0 60 60 0 0 1 120 0z"
|
||||
style="fill:#ffa000; stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 186v80c110 0 199 89 199 199h80c0-154-125-279-279-279z"
|
||||
style="fill:#ffa000; stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 47v79c187 0 338 152 338 339h80C465 234 277 47 47 47z"
|
||||
style="fill:#ffa000; stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<!-- Glass Shadow Effect Layer -->
|
||||
<g filter="url(#shadow)">
|
||||
<path d="M167 406a60 60 0 1 1-120 0 60 60 0 0 1 120 0z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 186v80c110 0 199 89 199 199h80c0-154-125-279-279-279z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 47v79c187 0 338 152 338 339h80C465 234 277 47 47 47z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg height="600" width="600" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M167 406a60 60 0 1 1-120 0 60 60 0 0 1 120 0zm0 0" style="fill:#ffa000" transform="translate(44 44)"/><path d="M47 186v80c110 0 199 89 199 199h80c0-154-125-279-279-279zm0 0" style="fill:#ffa000" transform="translate(44 44)"/><path d="M47 47v79c187 0 338 152 338 339h80C465 234 277 47 47 47Zm0 0" style="fill:#ffa000" transform="translate(44 44)"/></svg>
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 448 B |
|
@ -1,48 +0,0 @@
|
|||
<svg height="600" width="600" xmlns="http://www.w3.org/2000/svg" xml:space="preserve">
|
||||
<defs>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="5" />
|
||||
<feOffset dx="5" dy="5" result="offsetblur" />
|
||||
<feFlood flood-color="rgba(0,0,0,0.5)" />
|
||||
<feComposite in2="offsetblur" operator="in" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<!-- Glass Gradient -->
|
||||
<linearGradient id="glassGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:rgba(255, 255, 255, 0.3); stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:rgba(255, 255, 255, 0.1); stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Black shapes with orange margins -->
|
||||
<path d="M167 406a60 60 0 1 1-120 0 60 60 0 0 1 120 0z"
|
||||
style="fill:#000000; stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 186v80c110 0 199 89 199 199h80c0-154-125-279-279-279z"
|
||||
style="fill:#000000; stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 47v79c187 0 338 152 338 339h80C465 234 277 47 47 47z"
|
||||
style="fill:#000000; stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<!-- Glass Shadow Effect Layer -->
|
||||
<g filter="url(#shadow)">
|
||||
<path d="M167 406a60 60 0 1 1-120 0 60 60 0 0 1 120 0z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 186v80c110 0 199 89 199 199h80c0-154-125-279-279-279z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 47v79c187 0 338 152 338 339h80C465 234 277 47 47 47z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2 KiB |
|
@ -1,51 +0,0 @@
|
|||
<svg height="600" width="600" xmlns="http://www.w3.org/2000/svg" xml:space="preserve">
|
||||
<defs>
|
||||
<!-- Gradient for Glass Effect -->
|
||||
<linearGradient id="glassGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:rgba(255, 255, 255, 0.5); stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:rgba(0, 0, 0, 0.2); stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="glassHighlight" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:rgba(255, 255, 255, 0.8); stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:rgba(255, 255, 255, 0); stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="5" />
|
||||
<feOffset dx="5" dy="5" result="offsetblur" />
|
||||
<feFlood flood-color="rgba(0, 0, 0, 0.3)" />
|
||||
<feComposite in2="offsetblur" operator="in" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Shapes with Glass Effect -->
|
||||
<path d="M167 406a60 60 0 1 1-120 0 60 60 0 0 1 120 0z"
|
||||
style="fill:url(#glassGradient); stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 186v80c110 0 199 89 199 199h80c0-154-125-279-279-279z"
|
||||
style="fill:url(#glassGradient); stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 47v79c187 0 338 152 338 339h80C465 234 277 47 47 47z"
|
||||
style="fill:url(#glassGradient); stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<!-- Highlights for Shiny Effect -->
|
||||
<path d="M167 406a60 60 0 1 1-120 0 60 60 0 0 1 120 0z"
|
||||
style="fill:url(#glassHighlight); opacity:0.6;"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 186v80c110 0 199 89 199 199h80c0-154-125-279-279-279z"
|
||||
style="fill:url(#glassHighlight); opacity:0.6;"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 47v79c187 0 338 152 338 339h80C465 234 277 47 47 47z"
|
||||
style="fill:url(#glassHighlight); opacity:0.6;"
|
||||
transform="translate(44 44)" />
|
||||
</svg>
|
Before Width: | Height: | Size: 2.2 KiB |
|
@ -1,48 +0,0 @@
|
|||
<svg height="600" width="600" xmlns="http://www.w3.org/2000/svg" xml:space="preserve">
|
||||
<defs>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="5" />
|
||||
<feOffset dx="5" dy="5" result="offsetblur" />
|
||||
<feFlood flood-color="rgba(0,0,0,0.5)" />
|
||||
<feComposite in2="offsetblur" operator="in" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<!-- Glass Gradient -->
|
||||
<linearGradient id="glassGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:rgba(255, 255, 255, 0.3); stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:rgba(255, 255, 255, 0.1); stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Black shapes with orange margins -->
|
||||
<path d="M167 406a60 60 0 1 1-120 0 60 60 0 0 1 120 0z"
|
||||
style="fill:url(#glassGradient); stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 186v80c110 0 199 89 199 199h80c0-154-125-279-279-279z"
|
||||
style="fill:url(#glassGradient); stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 47v79c187 0 338 152 338 339h80C465 234 277 47 47 47z"
|
||||
style="fill:url(#glassGradient); stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<!-- Glass Shadow Effect Layer -->
|
||||
<g filter="url(#shadow)">
|
||||
<path d="M167 406a60 60 0 1 1-120 0 60 60 0 0 1 120 0z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 186v80c110 0 199 89 199 199h80c0-154-125-279-279-279z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 47v79c187 0 338 152 338 339h80C465 234 277 47 47 47z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2 KiB |
|
@ -1,48 +0,0 @@
|
|||
<svg height="600" width="600" xmlns="http://www.w3.org/2000/svg" xml:space="preserve">
|
||||
<defs>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="5" />
|
||||
<feOffset dx="5" dy="5" result="offsetblur" />
|
||||
<feFlood flood-color="rgba(0,0,0,0.5)" />
|
||||
<feComposite in2="offsetblur" operator="in" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<!-- Glass Gradient -->
|
||||
<linearGradient id="glassGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:rgba(255, 255, 255, 0.3); stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:rgba(255, 255, 255, 0.1); stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Black shapes with orange margins -->
|
||||
<path d="M167 406a60 60 0 1 1-120 0 60 60 0 0 1 120 0z"
|
||||
style="fill:#ffffff; stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 186v80c110 0 199 89 199 199h80c0-154-125-279-279-279z"
|
||||
style="fill:#ffffff; stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 47v79c187 0 338 152 338 339h80C465 234 277 47 47 47z"
|
||||
style="fill:#ffffff; stroke:#e15a00; stroke-width:10; filter:url(#shadow);"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<!-- Glass Shadow Effect Layer -->
|
||||
<g filter="url(#shadow)">
|
||||
<path d="M167 406a60 60 0 1 1-120 0 60 60 0 0 1 120 0z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 186v80c110 0 199 89 199 199h80c0-154-125-279-279-279z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
|
||||
<path d="M47 47v79c187 0 338 152 338 339h80C465 234 277 47 47 47z"
|
||||
style="fill:url(#glassGradient); stroke:none;"
|
||||
transform="translate(44 44)" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2 KiB |
|
@ -3,7 +3,7 @@ info = """
|
|||
Slixfeed is a news broker bot for syndicated news which aims to be \
|
||||
an easy to use and fully-featured news aggregating bot.
|
||||
|
||||
Slixfeed provides a convenient access to Blogs, News websites and \
|
||||
Slixfeed provides a convenient access to Blogs, News sites and \
|
||||
even Fediverse instances, along with filtering and other privacy \
|
||||
driven functionalities.
|
||||
|
|
@ -11,7 +11,6 @@ interval = 300 # Update interval (Minimum value 10)
|
|||
length = 300 # Maximum length of summary (Value 0 to disable)
|
||||
media = 0 # Display media (audio, image, video) when available
|
||||
old = 0 # Mark entries of newly added entries as unread
|
||||
omemo = 1 # Encrypt messages with OMEMO
|
||||
quantum = 3 # Amount of entries per update
|
||||
random = 0 # Pick random item from database
|
||||
|
|
@ -3,6 +3,12 @@
|
|||
|
||||
"""
|
||||
|
||||
FIXME
|
||||
|
||||
1) Use dict for ConfigDefault
|
||||
|
||||
2) Store ConfigJabberID in dicts
|
||||
|
||||
TODO
|
||||
|
||||
1) Site-specific filter (i.e. audiobookbay).
|
||||
|
@ -15,6 +21,14 @@ TODO
|
|||
|
||||
4) Copy file from /etc/slixfeed/ or /usr/share/slixfeed/
|
||||
|
||||
5) Merge get_value_default into get_value.
|
||||
|
||||
6) Use TOML https://ruudvanasseldonk.com/2023/01/11/the-yaml-document-from-hell
|
||||
|
||||
7) Make the program portable (directly use the directory assets) -- Thorsten
|
||||
|
||||
7.1) Read missing files from base directories or either set error message.
|
||||
|
||||
"""
|
||||
|
||||
import configparser
|
||||
|
@ -31,157 +45,33 @@ except:
|
|||
|
||||
logger = Logger(__name__)
|
||||
|
||||
class Settings:
|
||||
|
||||
def get_directory():
|
||||
"""
|
||||
Determine the directory path where setting files be stored.
|
||||
|
||||
* If $XDG_CONFIG_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to configuration directory.
|
||||
"""
|
||||
# config_home = xdg.BaseDirectory.xdg_config_home
|
||||
config_home = os.environ.get('XDG_CONFIG_HOME')
|
||||
if config_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
config_home = os.environ.get('APPDATA')
|
||||
if config_home is None:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
config_home = os.path.join(
|
||||
os.environ.get('HOME'), '.config'
|
||||
)
|
||||
return os.path.join(config_home, 'slixfeed')
|
||||
|
||||
|
||||
class Share:
|
||||
|
||||
def get_directory():
|
||||
"""
|
||||
Determine the directory path where data files be stored.
|
||||
|
||||
* If $XDG_DATA_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to database file.
|
||||
"""
|
||||
# data_home = xdg.BaseDirectory.xdg_data_home
|
||||
data_home = os.environ.get('XDG_DATA_HOME')
|
||||
if data_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
data_home = os.environ.get('APPDATA')
|
||||
if data_home is None:
|
||||
return os.path.abspath('.slixfeed/data')
|
||||
else:
|
||||
return os.path.abspath('.slixfeed/data')
|
||||
else:
|
||||
data_home = os.path.join(
|
||||
os.environ.get('HOME'), '.local', 'share'
|
||||
)
|
||||
return os.path.join(data_home, 'slixfeed')
|
||||
|
||||
class Cache:
|
||||
|
||||
def get_directory():
|
||||
"""
|
||||
Determine the directory path where cache files be stored.
|
||||
|
||||
* If $XDG_CACHE_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to cache directory.
|
||||
"""
|
||||
# cache_home = xdg.BaseDirectory.xdg_cache_home
|
||||
cache_home = os.environ.get('XDG_CACHE_HOME')
|
||||
if cache_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
cache_home = os.environ.get('APPDATA')
|
||||
if cache_home is None:
|
||||
return os.path.abspath('.slixfeed/cache')
|
||||
else:
|
||||
return os.path.abspath('.slixfeed/cache')
|
||||
else:
|
||||
cache_home = os.path.join(
|
||||
os.environ.get('HOME'), '.cache'
|
||||
)
|
||||
return os.path.join(cache_home, 'slixfeed')
|
||||
|
||||
|
||||
# TODO Consider a class ConfigDefault for default values to be initiate at most
|
||||
# basic level possible and a class ConfigJID for each JID (i.e. db_file) to be
|
||||
# also initiated at same level or at least at event call, then check whether
|
||||
# setting_jid.setting_key has value, otherwise resort to setting_default.setting_key.
|
||||
class Config:
|
||||
|
||||
def get_directory():
|
||||
"""
|
||||
Determine the directory path where setting files be stored.
|
||||
|
||||
* If $XDG_CONFIG_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to configuration directory.
|
||||
"""
|
||||
# config_home = xdg.BaseDirectory.xdg_config_home
|
||||
config_home = os.environ.get('XDG_CONFIG_HOME')
|
||||
if config_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
config_home = os.environ.get('APPDATA')
|
||||
if config_home is None:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
config_home = os.path.join(
|
||||
os.environ.get('HOME'), '.config'
|
||||
)
|
||||
return os.path.join(config_home, 'slixfeed')
|
||||
|
||||
def update_toml_file(filename, data):
|
||||
with open(filename, 'w') as new_file:
|
||||
content = tomli_w.dumps(data)
|
||||
new_file.write(content)
|
||||
def add_settings_default(settings):
|
||||
settings_default = get_values('settings.toml', 'settings')
|
||||
settings['default'] = settings_default
|
||||
|
||||
# TODO Open SQLite file once
|
||||
def add_settings_jid(self, jid_bare, db_file):
|
||||
self.settings[jid_bare] = {}
|
||||
for key in self.defaults['default']:
|
||||
def add_settings_jid(settings, jid_bare, db_file):
|
||||
settings[jid_bare] = {}
|
||||
for key in ('archive', 'enabled', 'filter', 'formatting', 'interval',
|
||||
'length', 'media', 'old', 'quantum'):
|
||||
value = sqlite.get_setting_value(db_file, key)
|
||||
if value:
|
||||
self.settings[jid_bare][key] = value[0]
|
||||
elif key not in ('check', 'formatting'):
|
||||
# NOTE This might neglects the need for
|
||||
# self.defaults of get_setting_value
|
||||
self.settings[jid_bare][key] = self.defaults['default'][key]
|
||||
if value: settings[jid_bare][key] = value[0]
|
||||
|
||||
async def set_setting_value(self, jid_bare, db_file, key, val):
|
||||
def get_settings_xmpp(key=None):
|
||||
result = get_values('accounts.toml', 'xmpp')
|
||||
result = result[key] if key else result
|
||||
return result
|
||||
|
||||
async def set_setting_value(settings, jid_bare, db_file, key, val):
|
||||
key = key.lower()
|
||||
key_val = [key, val]
|
||||
self.settings[jid_bare][key] = val
|
||||
settings[jid_bare][key] = val
|
||||
if sqlite.is_setting_key(db_file, key):
|
||||
await sqlite.update_setting_value(db_file, key_val)
|
||||
else:
|
||||
|
@ -189,55 +79,30 @@ class Config:
|
|||
|
||||
# TODO Segregate Jabber ID settings from Slixfeed wide settings.
|
||||
# self.settings, self.settings_xmpp, self.settings_irc etc.
|
||||
def get_setting_value(self, jid_bare, key):
|
||||
if jid_bare in self.settings and key in self.settings[jid_bare]:
|
||||
value = self.settings[jid_bare][key]
|
||||
def get_setting_value(settings, jid_bare, key):
|
||||
if jid_bare in settings and key in settings[jid_bare]:
|
||||
value = settings[jid_bare][key]
|
||||
else:
|
||||
value = self.defaults['default'][key]
|
||||
value = settings['default'][key]
|
||||
return value
|
||||
|
||||
|
||||
class Data:
|
||||
class ConfigNetwork:
|
||||
def __init__(self, settings):
|
||||
settings['network'] = {}
|
||||
for key in ('http_proxy', 'user_agent'):
|
||||
value = get_value('settings', 'Network', key)
|
||||
settings['network'][key] = value
|
||||
|
||||
|
||||
def get_directory():
|
||||
"""
|
||||
Determine the directory path where dbfile will be stored.
|
||||
|
||||
* If $XDG_DATA_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to database file.
|
||||
|
||||
Note
|
||||
----
|
||||
This function was taken from project buku.
|
||||
|
||||
See https://github.com/jarun/buku
|
||||
|
||||
* Arun Prakash Jana (jarun)
|
||||
* Dmitry Marakasov (AMDmi3)
|
||||
"""
|
||||
# data_home = xdg.BaseDirectory.xdg_data_home
|
||||
data_home = os.environ.get('XDG_DATA_HOME')
|
||||
if data_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
data_home = os.environ.get('APPDATA')
|
||||
if data_home is None:
|
||||
return os.path.abspath('.slixfeed/data')
|
||||
else:
|
||||
return os.path.abspath('.slixfeed/data')
|
||||
else:
|
||||
data_home = os.path.join(
|
||||
os.environ.get('HOME'), '.local', 'share'
|
||||
)
|
||||
return os.path.join(data_home, 'slixfeed')
|
||||
class ConfigJabberID:
|
||||
def __init__(self, settings, jid_bare, db_file):
|
||||
settings[jid_bare] = {}
|
||||
for key in ('archive', 'enabled', 'filter', 'formatting', 'interval',
|
||||
'length', 'media', 'old', 'quantum'):
|
||||
value = sqlite.get_setting_value(db_file, key)
|
||||
if value: value = value[0]
|
||||
print(value)
|
||||
settings[jid_bare][key] = value
|
||||
|
||||
|
||||
def get_values(filename, key=None):
|
||||
|
@ -271,6 +136,78 @@ def get_setting_value(db_file, key):
|
|||
return value
|
||||
|
||||
|
||||
# TODO Merge with backup_obsolete
|
||||
def update_proxies(file, proxy_name, proxy_type, proxy_url, action='remove'):
|
||||
"""
|
||||
Add given URL to given list.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file : str
|
||||
Filename.
|
||||
proxy_name : str
|
||||
Proxy name.
|
||||
proxy_type : str
|
||||
Proxy title.
|
||||
proxy_url : str
|
||||
Proxy URL.
|
||||
action : str
|
||||
add or remove
|
||||
|
||||
Returns
|
||||
-------
|
||||
None.
|
||||
"""
|
||||
data = open_config_file('proxies.toml')
|
||||
proxy_list = data['proxies'][proxy_name][proxy_type]
|
||||
# breakpoint()
|
||||
print('####################### PROXY ######################')
|
||||
proxy_index = proxy_list.index(proxy_url)
|
||||
proxy_list.pop(proxy_index)
|
||||
with open(file, 'w') as new_file:
|
||||
content = tomli_w.dumps(data)
|
||||
new_file.write(content)
|
||||
|
||||
|
||||
# TODO Merge with update_proxies
|
||||
def backup_obsolete(file, proxy_name, proxy_type, proxy_url, action='add'):
|
||||
"""
|
||||
Add given URL to given list.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file : str
|
||||
Filename.
|
||||
proxy_name : str
|
||||
Proxy name.
|
||||
proxy_type : str
|
||||
Proxy title.
|
||||
proxy_url : str
|
||||
Proxy URL.
|
||||
action : str
|
||||
add or remove
|
||||
|
||||
Returns
|
||||
-------
|
||||
None.
|
||||
"""
|
||||
data = open_config_file('proxies_obsolete.toml')
|
||||
proxy_list = data['proxies'][proxy_name][proxy_type]
|
||||
proxy_list.extend([proxy_url])
|
||||
with open(file, 'w') as new_file:
|
||||
content = tomli_w.dumps(data)
|
||||
new_file.write(content)
|
||||
|
||||
|
||||
def create_skeleton(file):
|
||||
with open(file, 'rb') as original_file:
|
||||
data = tomllib.load(original_file)
|
||||
data = clear_values(data)
|
||||
with open('proxies_obsolete.toml', 'w') as new_file:
|
||||
content = tomli_w.dumps(data)
|
||||
new_file.write(content)
|
||||
|
||||
|
||||
def clear_values(input):
|
||||
if isinstance(input, dict):
|
||||
return {k: clear_values(v) for k, v in input.items()}
|
||||
|
@ -280,7 +217,262 @@ def clear_values(input):
|
|||
return ''
|
||||
|
||||
|
||||
def add_to_list(newwords, keywords):
|
||||
# TODO Return dict instead of list
|
||||
def get_value(filename, section, keys):
|
||||
"""
|
||||
Get setting value.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : str
|
||||
INI filename.
|
||||
keys : list or str
|
||||
A single key as string or multiple keys as list.
|
||||
section : str
|
||||
INI Section.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : list or str
|
||||
A single value as string or multiple values as list.
|
||||
"""
|
||||
result = None
|
||||
config_res = configparser.RawConfigParser()
|
||||
config_dir = get_default_config_directory()
|
||||
if not os.path.isdir(config_dir):
|
||||
config_dir = '/usr/share/slixfeed/'
|
||||
if not os.path.isdir(config_dir):
|
||||
config_dir = os.path.dirname(__file__) + "/assets"
|
||||
config_file = os.path.join(config_dir, filename + ".ini")
|
||||
config_res.read(config_file)
|
||||
if config_res.has_section(section):
|
||||
section_res = config_res[section]
|
||||
if isinstance(keys, list):
|
||||
result = []
|
||||
for key in keys:
|
||||
if key in section_res:
|
||||
value = section_res[key]
|
||||
logger.debug("Found value {} for key {}".format(value, key))
|
||||
else:
|
||||
value = ''
|
||||
logger.debug("Missing key:", key)
|
||||
result.extend([value])
|
||||
elif isinstance(keys, str):
|
||||
key = keys
|
||||
if key in section_res:
|
||||
result = section_res[key]
|
||||
logger.debug("Found value {} for key {}".format(result, key))
|
||||
else:
|
||||
result = ''
|
||||
# logger.error("Missing key:", key)
|
||||
if result == None:
|
||||
logger.error(
|
||||
"Check configuration file {}.ini for "
|
||||
"missing key(s) \"{}\" under section [{}].".format(
|
||||
filename, keys, section)
|
||||
)
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
# TODO Store config file as an object in runtime, otherwise
|
||||
# the file will be opened time and time again.
|
||||
# TODO Copy file from /etc/slixfeed/ or /usr/share/slixfeed/
|
||||
def get_value_default(filename, section, key):
|
||||
"""
|
||||
Get settings default value.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key : str
|
||||
Key: archive, enabled, interval,
|
||||
length, old, quantum, random.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : str
|
||||
Value.
|
||||
"""
|
||||
config_res = configparser.RawConfigParser()
|
||||
config_dir = get_default_config_directory()
|
||||
if not os.path.isdir(config_dir):
|
||||
config_dir = '/usr/share/slixfeed/'
|
||||
config_file = os.path.join(config_dir, filename + ".ini")
|
||||
config_res.read(config_file)
|
||||
if config_res.has_section(section):
|
||||
result = config_res[section][key]
|
||||
return result
|
||||
|
||||
|
||||
# TODO DELETE THIS FUNCTION OR KEEP ONLY THE CODE BELOW NOTE
|
||||
# IF CODE BELOW NOTE IS KEPT, RENAME FUNCTION TO open_toml
|
||||
def open_config_file(filename):
|
||||
"""
|
||||
Get settings default value.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : str
|
||||
Filename of toml file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : list
|
||||
List of pathnames or keywords.
|
||||
"""
|
||||
config_dir = get_default_config_directory()
|
||||
if not os.path.isdir(config_dir):
|
||||
config_dir = '/usr/share/slixfeed/'
|
||||
if not os.path.isdir(config_dir):
|
||||
config_dir = os.path.dirname(__file__) + "/assets"
|
||||
config_file = os.path.join(config_dir, filename)
|
||||
# NOTE THIS IS THE IMPORTANT CODE
|
||||
with open(config_file, mode="rb") as defaults:
|
||||
# default = yaml.safe_load(defaults)
|
||||
# result = default[key]
|
||||
result = tomllib.load(defaults)
|
||||
return result
|
||||
|
||||
|
||||
def get_default_data_directory():
|
||||
"""
|
||||
Determine the directory path where dbfile will be stored.
|
||||
|
||||
* If $XDG_DATA_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to database file.
|
||||
|
||||
Note
|
||||
----
|
||||
This function was taken from project buku.
|
||||
|
||||
See https://github.com/jarun/buku
|
||||
|
||||
* Arun Prakash Jana (jarun)
|
||||
* Dmitry Marakasov (AMDmi3)
|
||||
"""
|
||||
# data_home = xdg.BaseDirectory.xdg_data_home
|
||||
data_home = os.environ.get('XDG_DATA_HOME')
|
||||
if data_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
data_home = os.environ.get('APPDATA')
|
||||
if data_home is None:
|
||||
return os.path.abspath('.slixfeed/data')
|
||||
else:
|
||||
return os.path.abspath('.slixfeed/data')
|
||||
else:
|
||||
data_home = os.path.join(
|
||||
os.environ.get('HOME'), '.local', 'share'
|
||||
)
|
||||
return os.path.join(data_home, 'slixfeed')
|
||||
|
||||
|
||||
def get_default_cache_directory():
|
||||
"""
|
||||
Determine the directory path where dbfile will be stored.
|
||||
|
||||
* If $XDG_DATA_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to cache directory.
|
||||
"""
|
||||
# data_home = xdg.BaseDirectory.xdg_data_home
|
||||
data_home = os.environ.get('XDG_CACHE_HOME')
|
||||
if data_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
data_home = os.environ.get('APPDATA')
|
||||
if data_home is None:
|
||||
return os.path.abspath('.slixfeed/cache')
|
||||
else:
|
||||
return os.path.abspath('.slixfeed/cache')
|
||||
else:
|
||||
data_home = os.path.join(
|
||||
os.environ.get('HOME'), '.cache'
|
||||
)
|
||||
return os.path.join(data_home, 'slixfeed')
|
||||
|
||||
|
||||
# TODO Write a similar function for file.
|
||||
# NOTE the is a function of directory, noot file.
|
||||
def get_default_config_directory():
|
||||
"""
|
||||
Determine the directory path where configuration will be stored.
|
||||
|
||||
* If $XDG_CONFIG_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to configuration directory.
|
||||
"""
|
||||
# config_home = xdg.BaseDirectory.xdg_config_home
|
||||
config_home = os.environ.get('XDG_CONFIG_HOME')
|
||||
if config_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
config_home = os.environ.get('APPDATA')
|
||||
if config_home is None:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
config_home = os.path.join(
|
||||
os.environ.get('HOME'), '.config'
|
||||
)
|
||||
return os.path.join(config_home, 'slixfeed')
|
||||
|
||||
|
||||
def get_pathname_to_database(jid_file):
|
||||
"""
|
||||
Callback function to instantiate action on database.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jid_file : str
|
||||
Filename.
|
||||
callback : ?
|
||||
Function name.
|
||||
message : str, optional
|
||||
Optional kwarg when a message is a part or
|
||||
required argument. The default is None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
object
|
||||
Coroutine object.
|
||||
"""
|
||||
db_dir = get_default_data_directory()
|
||||
if not os.path.isdir(db_dir):
|
||||
os.mkdir(db_dir)
|
||||
if not os.path.isdir(db_dir + "/sqlite"):
|
||||
os.mkdir(db_dir + "/sqlite")
|
||||
db_file = os.path.join(db_dir, "sqlite", r"{}.db".format(jid_file))
|
||||
sqlite.create_tables(db_file)
|
||||
return db_file
|
||||
# await set_default_values(db_file)
|
||||
# if message:
|
||||
# return await callback(db_file, message)
|
||||
# else:
|
||||
# return await callback(db_file)
|
||||
|
||||
|
||||
async def add_to_list(newwords, keywords):
|
||||
"""
|
||||
Append new keywords to list.
|
||||
|
||||
|
@ -311,7 +503,7 @@ def add_to_list(newwords, keywords):
|
|||
return val
|
||||
|
||||
|
||||
def remove_from_list(newwords, keywords):
|
||||
async def remove_from_list(newwords, keywords):
|
||||
"""
|
||||
Remove given keywords from list.
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
proxies = {}
|
|
@ -1,28 +0,0 @@
|
|||
enabled = 0
|
||||
blacklist = [
|
||||
"christian@iceagefarmer.com",
|
||||
"david@falkon.org",
|
||||
"diggy@diggy.club",
|
||||
"dino@drdino.com",
|
||||
"doug@baxterisland.com",
|
||||
"doug@blacklistednews.com",
|
||||
"emdek@otter-browser.org",
|
||||
"eric@drdino.com",
|
||||
"gemini@circumlunar.space",
|
||||
"hal@reallibertymedia.com",
|
||||
"henrik@redice.tv",
|
||||
"jg@cwns.i2p",
|
||||
"jo@drdino.com",
|
||||
"joel@thelunaticfarmer.com",
|
||||
"kent@drdino.com",
|
||||
"lana@redice.tv",
|
||||
"larken@larkenrose.com",
|
||||
"lee@oraclebroadcasting.com",
|
||||
"mark@enclosedworld.com",
|
||||
"mark@marksargent.com",
|
||||
"nick@nightnationreview.com",
|
||||
"oliver@postmarketos.org",
|
||||
"robert@cwns.i2p",
|
||||
"patrick@slackware.com",
|
||||
]
|
||||
whitelist = []
|
|
@ -1,56 +0,0 @@
|
|||
# Set Slixfeed Ad-Hoc Commands in MUC
|
||||
|
||||
This documents provides instructions for setting Slixfeed Ad-Hoc Commands on your XMPP server
|
||||
|
||||
These instruction are currently applied only to Prosody XMPP server.
|
||||
|
||||
We encourage to contribute instructions for other XMPP servers.
|
||||
|
||||
## Prosody
|
||||
|
||||
First of all install the relative Community Module:
|
||||
|
||||
```
|
||||
$ sudo prosodyctl install --server=https://modules.prosody.im/rocks/ mod_muc_adhoc_bots
|
||||
```
|
||||
|
||||
Then enable the module in your **MUC component** (`/etc/prosody/prosody.cfg.lua`), like this:
|
||||
|
||||
```
|
||||
modules_enabled = {
|
||||
"muc_mam",
|
||||
"vcard_muc",
|
||||
…
|
||||
"muc_adhoc_bots",
|
||||
…
|
||||
"server_contact_info"
|
||||
}
|
||||
```
|
||||
|
||||
Last part is the bot's configuration, which goes again under the MUC component settings:
|
||||
|
||||
```
|
||||
adhoc_bots = { "bot@jabber.i2p/slixfeed" }
|
||||
```
|
||||
|
||||
Substitute `bot@jabber.i2p/slixfeed` with your bot JID and device name which has to correspond to `accounts.toml` settings for Slixfeed configuration:
|
||||
|
||||
```
|
||||
[xmpp.client]
|
||||
alias = "Slixfeed"
|
||||
jid = "bot@jabber.i2p/slixfeed"
|
||||
```
|
||||
|
||||
Reload the Prosody config and then load the module you just enabled under MUC component, or simply restart the XMPP server.
|
||||
|
||||
```
|
||||
$ sudo prosodyctl shell
|
||||
|
||||
prosody> config:reload()
|
||||
prosody> module:load('muc_adhoc_bots', "muc_component.jabber.i2p")
|
||||
prosody> bye
|
||||
```
|
||||
|
||||
Authors:
|
||||
|
||||
- Simone Canaletti (roughnecks)
|
|
@ -36,17 +36,15 @@ NOTE
|
|||
|
||||
"""
|
||||
|
||||
import aiofiles
|
||||
from aiohttp import ClientError, ClientSession, ClientTimeout
|
||||
from asyncio import TimeoutError
|
||||
# from asyncio.exceptions import IncompleteReadError
|
||||
# from http.client import IncompleteRead
|
||||
# from lxml import html
|
||||
# from xml.etree.ElementTree import ElementTree, ParseError
|
||||
#import requests
|
||||
import requests
|
||||
import slixfeed.config as config
|
||||
from slixfeed.log import Logger
|
||||
# import urllib.request
|
||||
# from urllib.error import HTTPError
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
@ -57,6 +55,7 @@ except:
|
|||
"Package magnet2torrent was not found.\n"
|
||||
"BitTorrent is disabled.")
|
||||
|
||||
|
||||
# class Dat:
|
||||
# async def dat():
|
||||
|
||||
|
@ -69,152 +68,55 @@ except:
|
|||
# class Gopher:
|
||||
# async def gopher():
|
||||
|
||||
# class Http:
|
||||
# async def http():
|
||||
|
||||
# class Ipfs:
|
||||
# async def ipfs():
|
||||
|
||||
|
||||
class Http:
|
||||
def http_response(url):
|
||||
"""
|
||||
Download response headers.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url : str
|
||||
URL.
|
||||
|
||||
Returns
|
||||
-------
|
||||
response: requests.models.Response
|
||||
HTTP Header Response.
|
||||
|
||||
Result would contain these:
|
||||
response.encoding
|
||||
response.headers
|
||||
response.history
|
||||
response.reason
|
||||
response.status_code
|
||||
response.url
|
||||
"""
|
||||
user_agent = (
|
||||
config.get_value(
|
||||
"settings", "Network", "user_agent")
|
||||
) or 'Slixfeed/0.1'
|
||||
headers = {
|
||||
"User-Agent": user_agent
|
||||
}
|
||||
try:
|
||||
# Do not use HEAD request because it appears that too many sites would
|
||||
# deny it.
|
||||
# response = requests.head(url, headers=headers, allow_redirects=True)
|
||||
response = requests.get(url, headers=headers, allow_redirects=True)
|
||||
except Exception as e:
|
||||
logger.warning('Error in HTTP response')
|
||||
logger.error(e)
|
||||
response = None
|
||||
return response
|
||||
|
||||
|
||||
# def fetch_media(url, pathname):
|
||||
# try:
|
||||
# urllib.request.urlretrieve(url, pathname)
|
||||
# status = 1
|
||||
# except HTTPError as e:
|
||||
# logger.error(e)
|
||||
# status = 0
|
||||
# return status
|
||||
|
||||
|
||||
async def fetch_headers(settings_network, url):
|
||||
user_agent = (settings_network['user_agent'] or 'Slixfeed/0.1')
|
||||
headers = {'User-Agent': user_agent}
|
||||
proxy = (settings_network['http_proxy'] or None)
|
||||
timeout = ClientTimeout(total=10)
|
||||
async with ClientSession(headers=headers) as session:
|
||||
async with session.get(url, proxy=proxy,
|
||||
# proxy_auth=(proxy_username, proxy_password),
|
||||
timeout=timeout
|
||||
) as response:
|
||||
headers = response.headers
|
||||
return headers
|
||||
# print("Headers for URL:", url)
|
||||
# for header_name, header_value in headers.items():
|
||||
# print(f"{header_name}: {header_value}")
|
||||
|
||||
|
||||
# TODO Write file to disk. Consider aiofiles
|
||||
async def fetch_media(settings_network, url, pathname):
|
||||
"""
|
||||
Download media content of given URL.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url : str
|
||||
URL.
|
||||
pathname : list
|
||||
Pathname (including filename) to save content to.
|
||||
|
||||
Returns
|
||||
-------
|
||||
msg: list or str
|
||||
Document or error message.
|
||||
"""
|
||||
user_agent = (settings_network['user_agent'] or 'Slixfeed/0.1')
|
||||
headers = {'User-Agent': user_agent}
|
||||
proxy = (settings_network['http_proxy'] or None)
|
||||
timeout = ClientTimeout(total=10)
|
||||
async with ClientSession(headers=headers) as session:
|
||||
# async with ClientSession(trust_env=True) as session:
|
||||
try:
|
||||
async with session.get(url, proxy=proxy,
|
||||
# proxy_auth=(proxy_username, proxy_password),
|
||||
timeout=timeout
|
||||
) as response:
|
||||
status = response.status
|
||||
if status in (200, 201):
|
||||
f = await aiofiles.open(pathname, mode='wb')
|
||||
await f.write(await response.read())
|
||||
await f.close()
|
||||
try:
|
||||
result = {'charset': response.charset,
|
||||
'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:
|
||||
result = {'error': True,
|
||||
'message': 'Could not get document.',
|
||||
'original_url': url,
|
||||
'status_code': status,
|
||||
'response_url': response.url}
|
||||
else:
|
||||
result = {'error': True,
|
||||
'message': 'HTTP Error:' + str(status),
|
||||
'original_url': url,
|
||||
'status_code': status,
|
||||
'response_url': response.url}
|
||||
except ClientError as e:
|
||||
result = {'error': True,
|
||||
'message': 'Error:' + str(e) if e else 'ClientError',
|
||||
'original_url': url,
|
||||
'status_code': None}
|
||||
except TimeoutError as e:
|
||||
result = {'error': True,
|
||||
'message': 'Timeout:' + str(e) if e else 'TimeoutError',
|
||||
'original_url': url,
|
||||
'status_code': None}
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
result = {'error': True,
|
||||
'message': 'Error:' + str(e) if e else 'Error',
|
||||
'original_url': url,
|
||||
'status_code': None}
|
||||
return result
|
||||
|
||||
|
||||
def http_response(settings_network, url):
|
||||
"""
|
||||
Download response headers.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url : str
|
||||
URL.
|
||||
|
||||
Returns
|
||||
-------
|
||||
response: requests.models.Response
|
||||
HTTP Header Response.
|
||||
|
||||
Result would contain these:
|
||||
response.encoding
|
||||
response.headers
|
||||
response.history
|
||||
response.reason
|
||||
response.status_code
|
||||
response.url
|
||||
"""
|
||||
user_agent = settings_network['user_agent'] or 'Slixfeed/0.1'
|
||||
headers = {
|
||||
"User-Agent": user_agent
|
||||
}
|
||||
try:
|
||||
# Do not use HEAD request because it appears that too many sites would
|
||||
# deny it.
|
||||
# response = requests.head(url, headers=headers, allow_redirects=True)
|
||||
response = requests.get(url, headers=headers, allow_redirects=True)
|
||||
except Exception as e:
|
||||
logger.warning('Error in HTTP response')
|
||||
logger.error(e)
|
||||
response = None
|
||||
return response
|
||||
|
||||
|
||||
async def http(settings_network, url):
|
||||
async def http(url):
|
||||
"""
|
||||
Download content of given URL.
|
||||
|
||||
|
@ -228,9 +130,10 @@ async def http(settings_network, url):
|
|||
msg: list or str
|
||||
Document or error message.
|
||||
"""
|
||||
user_agent = (settings_network['user_agent'] or 'Slixfeed/0.1')
|
||||
network_settings = config.get_values('settings.toml', 'network')
|
||||
user_agent = (network_settings['user_agent'] or 'Slixfeed/0.1')
|
||||
headers = {'User-Agent': user_agent}
|
||||
proxy = (settings_network['http_proxy'] or None)
|
||||
proxy = (network_settings['http_proxy'] or None)
|
||||
timeout = ClientTimeout(total=10)
|
||||
async with ClientSession(headers=headers) as session:
|
||||
# async with ClientSession(trust_env=True) as session:
|
||||
|
|
|
@ -19,8 +19,6 @@ import logging
|
|||
|
||||
class Logger:
|
||||
|
||||
def set_logging_level(level):
|
||||
logging.basicConfig(level)
|
||||
|
||||
def __init__(self, name):
|
||||
self.logger = logging.getLogger(name)
|
||||
|
@ -60,5 +58,4 @@ class Message:
|
|||
def printer(text):
|
||||
now = datetime.now()
|
||||
current_time = now.strftime("%H:%M:%S")
|
||||
# print('{} {}'.format(current_time, text), end='\r')
|
||||
print('{} {}'.format(current_time, text))
|
||||
print('{} {}'.format(current_time, text), end='\r')
|
||||
|
|
|
@ -366,7 +366,7 @@ def create_tables(db_file):
|
|||
id INTEGER NOT NULL,
|
||||
feed_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
FOREIGN KEY ("feed_id") REFERENCES "feeds_properties" ("id")
|
||||
FOREIGN KEY ("feed_id") REFERENCES "feeds" ("id")
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY ("tag_id") REFERENCES "tags" ("id")
|
||||
|
@ -2762,39 +2762,6 @@ def get_active_feeds_url(db_file):
|
|||
return result
|
||||
|
||||
|
||||
def get_active_feeds_url_sorted_by_last_scanned(db_file):
|
||||
"""
|
||||
Query table feeds for active URLs and sort them by last scanned time.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Path to database file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : tuple
|
||||
URLs of active feeds.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: db_file: {}'
|
||||
.format(function_name, db_file))
|
||||
with create_connection(db_file) as conn:
|
||||
cur = conn.cursor()
|
||||
sql = (
|
||||
"""
|
||||
SELECT feeds_properties.url
|
||||
FROM feeds_properties
|
||||
INNER JOIN feeds_preferences ON feeds_properties.id = feeds_preferences.feed_id
|
||||
INNER JOIN feeds_state ON feeds_properties.id = feeds_state.feed_id
|
||||
WHERE feeds_preferences.enabled = 1
|
||||
ORDER BY feeds_state.scanned
|
||||
"""
|
||||
)
|
||||
result = cur.execute(sql).fetchall()
|
||||
return result
|
||||
|
||||
|
||||
def get_tags(db_file):
|
||||
"""
|
||||
Query table tags and list items.
|
||||
|
@ -3032,7 +2999,7 @@ def check_entry_exist(db_file, feed_id, identifier=None, title=None, link=None,
|
|||
"""
|
||||
SELECT id
|
||||
FROM entries_properties
|
||||
WHERE identifier = :identifier AND feed_id = :feed_id
|
||||
WHERE identifier = :identifier and feed_id = :feed_id
|
||||
"""
|
||||
)
|
||||
par = {
|
||||
|
|
|
@ -27,11 +27,12 @@ TODO
|
|||
import asyncio
|
||||
from feedparser import parse
|
||||
import os
|
||||
import slixfeed.config as config
|
||||
from slixfeed.config import Config
|
||||
import slixfeed.fetch as fetch
|
||||
from slixfeed.log import Logger,Message
|
||||
import slixfeed.sqlite as sqlite
|
||||
from slixfeed.utilities import Database, DateAndTime, Html, MD, String, Url, Utilities
|
||||
from slixfeed.utilities import DateAndTime, Html, MD, String, Url, Utilities
|
||||
from slixmpp.xmlstream import ET
|
||||
import sys
|
||||
from urllib.parse import urlsplit
|
||||
|
@ -43,16 +44,17 @@ logger = Logger(__name__)
|
|||
class Feed:
|
||||
|
||||
# NOTE Consider removal of MD (and any other option HTML and XBEL)
|
||||
def export_feeds(dir_data, dir_cache, jid_bare, ext):
|
||||
def export_feeds(jid_bare, ext):
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: jid_bare: {}: ext: {}'.format(function_name, jid_bare, ext))
|
||||
if not os.path.isdir(dir_cache):
|
||||
os.mkdir(dir_cache)
|
||||
if not os.path.isdir(dir_cache + '/' + ext):
|
||||
os.mkdir(dir_cache + '/' + ext)
|
||||
cache_dir = config.get_default_cache_directory()
|
||||
if not os.path.isdir(cache_dir):
|
||||
os.mkdir(cache_dir)
|
||||
if not os.path.isdir(cache_dir + '/' + ext):
|
||||
os.mkdir(cache_dir + '/' + ext)
|
||||
filename = os.path.join(
|
||||
dir_cache, ext, 'slixfeed_' + DateAndTime.timestamp() + '.' + ext)
|
||||
db_file = Database.instantiate(dir_data, jid_bare)
|
||||
cache_dir, ext, 'slixfeed_' + DateAndTime.timestamp() + '.' + ext)
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
results = sqlite.get_feeds(db_file)
|
||||
match ext:
|
||||
# case 'html':
|
||||
|
@ -272,7 +274,7 @@ class Feed:
|
|||
feed_id = sqlite.get_feed_id(db_file, url)
|
||||
if not feed_id:
|
||||
if not sqlite.check_identifier_exist(db_file, identifier):
|
||||
result = await fetch.http(self.settings_network, url)
|
||||
result = await fetch.http(url)
|
||||
message = result['message']
|
||||
status_code = result['status_code']
|
||||
if not result['error']:
|
||||
|
@ -346,9 +348,8 @@ class Feed:
|
|||
if new_entries:
|
||||
await sqlite.add_entries_and_update_feed_state(
|
||||
db_file, feed_id, new_entries)
|
||||
old = self.settings[jid_bare]['old'] or self.defaults['default']['old']
|
||||
if not old: await sqlite.mark_feed_as_read(db_file,
|
||||
feed_id)
|
||||
old = Config.get_setting_value(self.settings, jid_bare, 'old')
|
||||
if not old: await sqlite.mark_feed_as_read(db_file, feed_id)
|
||||
result_final = {'link' : url,
|
||||
'index' : feed_id,
|
||||
'name' : title,
|
||||
|
@ -362,8 +363,7 @@ class Feed:
|
|||
# NOTE Do not be tempted to return a compact dictionary.
|
||||
# That is, dictionary within dictionary
|
||||
# Return multiple dictionaries in a list or tuple.
|
||||
result = await FeedDiscovery.probe_page(
|
||||
self.settings_network, self.pathnames, url, document)
|
||||
result = await FeedDiscovery.probe_page(url, document)
|
||||
if not result:
|
||||
# Get out of the loop with dict indicating error.
|
||||
result_final = {'link' : url,
|
||||
|
@ -521,7 +521,7 @@ class Feed:
|
|||
|
||||
|
||||
# NOTE This function is not being utilized
|
||||
async def download_feed(settings_network, db_file, feed_url):
|
||||
async def download_feed(self, db_file, feed_url):
|
||||
"""
|
||||
Process feed content.
|
||||
|
||||
|
@ -536,7 +536,7 @@ class Feed:
|
|||
logger.debug('{}: db_file: {} url: {}'
|
||||
.format(function_name, db_file, feed_url))
|
||||
if isinstance(feed_url, tuple): feed_url = feed_url[0]
|
||||
result = await fetch.http(settings_network, feed_url)
|
||||
result = await fetch.http(feed_url)
|
||||
feed_id = sqlite.get_feed_id(db_file, feed_url)
|
||||
feed_id = feed_id[0]
|
||||
status_code = result['status_code']
|
||||
|
@ -933,7 +933,7 @@ class FeedDiscovery:
|
|||
# else:
|
||||
# return await callback(url)
|
||||
|
||||
async def probe_page(settings_network, pathnames, url, document=None):
|
||||
async def probe_page(url, document=None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
|
@ -948,7 +948,7 @@ class FeedDiscovery:
|
|||
Single URL as list or selection of URLs as str.
|
||||
"""
|
||||
if not document:
|
||||
response = await fetch.http(settings_network, url)
|
||||
response = await fetch.http(url)
|
||||
if not response['error']:
|
||||
document = response['content']
|
||||
try:
|
||||
|
@ -974,7 +974,7 @@ class FeedDiscovery:
|
|||
result = None
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
logger.warning(f"Failed to parse URL as feed for {url}.")
|
||||
logger.warning("Failed to parse URL as feed for {}.".format(url))
|
||||
result = {'link' : None,
|
||||
'index' : None,
|
||||
'name' : None,
|
||||
|
@ -982,23 +982,23 @@ class FeedDiscovery:
|
|||
'error' : True,
|
||||
'exist' : None}
|
||||
if not result:
|
||||
logger.debug(f"Feed auto-discovery engaged for {url}")
|
||||
logger.debug("Feed auto-discovery engaged for {}".format(url))
|
||||
result = FeedDiscovery.feed_mode_auto_discovery(url, tree)
|
||||
if not result:
|
||||
logger.debug(f"Feed link scan mode engaged for {url}")
|
||||
result = FeedDiscovery.feed_mode_scan(url, tree, pathnames)
|
||||
logger.debug("Feed link scan mode engaged for {}".format(url))
|
||||
result = FeedDiscovery.feed_mode_scan(url, tree)
|
||||
if not result:
|
||||
logger.debug(f"Feed arbitrary mode engaged for {url}")
|
||||
result = FeedDiscovery.feed_mode_guess(url, pathnames)
|
||||
logger.debug("Feed arbitrary mode engaged for {}".format(url))
|
||||
result = FeedDiscovery.feed_mode_guess(url, tree)
|
||||
if not result:
|
||||
logger.debug(f"No feeds were found for {url}")
|
||||
logger.debug("No feeds were found for {}".format(url))
|
||||
result = None
|
||||
result = await FeedDiscovery.process_feed_selection(settings_network, url, result)
|
||||
result = await FeedDiscovery.process_feed_selection(url, result)
|
||||
return result
|
||||
|
||||
|
||||
# TODO Improve scan by gradual decreasing of path
|
||||
def feed_mode_guess(url, pathnames):
|
||||
def feed_mode_guess(url, tree):
|
||||
"""
|
||||
Lookup for feeds by pathname using HTTP Requests.
|
||||
|
||||
|
@ -1008,8 +1008,8 @@ class FeedDiscovery:
|
|||
Path to database file.
|
||||
url : str
|
||||
URL.
|
||||
pathnames : list
|
||||
pathnames.
|
||||
tree : TYPE
|
||||
DESCRIPTION.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
@ -1018,17 +1018,18 @@ class FeedDiscovery:
|
|||
"""
|
||||
urls = []
|
||||
parted_url = urlsplit(url)
|
||||
paths = config.open_config_file("lists.toml")["pathnames"]
|
||||
# Check whether URL has path (i.e. not root)
|
||||
# Check parted_url.path to avoid error in case root wasn't given
|
||||
# TODO Make more tests
|
||||
if parted_url.path and parted_url.path.split('/')[1]:
|
||||
pathnames.extend(
|
||||
paths.extend(
|
||||
[".atom", ".feed", ".rdf", ".rss"]
|
||||
) if '.rss' not in pathnames else -1
|
||||
) 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 pathnames:
|
||||
for path in paths:
|
||||
address = Url.join_url(url, parted_url_path.split('/')[1] + path)
|
||||
if address not in urls:
|
||||
urls.extend([address])
|
||||
|
@ -1037,7 +1038,7 @@ class FeedDiscovery:
|
|||
return urls
|
||||
|
||||
|
||||
def feed_mode_scan(url, tree, pathnames):
|
||||
def feed_mode_scan(url, tree):
|
||||
"""
|
||||
Scan page for potential feeds by pathname.
|
||||
|
||||
|
@ -1056,7 +1057,8 @@ class FeedDiscovery:
|
|||
Message with URLs.
|
||||
"""
|
||||
urls = []
|
||||
for path in pathnames:
|
||||
paths = config.open_config_file("lists.toml")["pathnames"]
|
||||
for path in paths:
|
||||
# xpath_query = "//*[@*[contains(.,'{}')]]".format(path)
|
||||
# xpath_query = "//a[contains(@href,'{}')]".format(path)
|
||||
num = 5
|
||||
|
@ -1138,10 +1140,10 @@ class FeedDiscovery:
|
|||
# URLs (string) and Feeds (dict) and function that
|
||||
# composes text message (string).
|
||||
# Maybe that's not necessary.
|
||||
async def process_feed_selection(settings_network, url, urls):
|
||||
async def process_feed_selection(url, urls):
|
||||
feeds = {}
|
||||
for i in urls:
|
||||
result = await fetch.http(settings_network, i)
|
||||
result = await fetch.http(i)
|
||||
if not result['error']:
|
||||
document = result['content']
|
||||
status_code = result['status_code']
|
||||
|
@ -1273,10 +1275,10 @@ class FeedTask:
|
|||
# print('Scanning for updates for JID {}'.format(jid_bare))
|
||||
logger.info('Scanning for updates for JID {}'.format(jid_bare))
|
||||
while True:
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
urls = sqlite.get_active_feeds_url_sorted_by_last_scanned(db_file)
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
urls = sqlite.get_active_feeds_url(db_file)
|
||||
for url in urls:
|
||||
#Message.printer('Scanning updates for URL {} ...'.format(url))
|
||||
Message.printer('Scanning updates for URL {} ...'.format(url))
|
||||
url = url[0]
|
||||
# print('STA',url)
|
||||
|
||||
|
@ -1286,7 +1288,7 @@ class FeedTask:
|
|||
# print('Skipping URL:', url)
|
||||
# continue
|
||||
|
||||
result = await fetch.http(self.settings_network, url)
|
||||
result = await fetch.http(url)
|
||||
status_code = result['status_code']
|
||||
feed_id = sqlite.get_feed_id(db_file, url)
|
||||
feed_id = feed_id[0]
|
||||
|
@ -1333,7 +1335,7 @@ class FeedTask:
|
|||
new_entries.extend([new_entry])
|
||||
if new_entries:
|
||||
await sqlite.add_entries_and_update_feed_state(db_file, feed_id, new_entries)
|
||||
limit = Config.get_setting_value(self, jid_bare, 'archive')
|
||||
limit = Config.get_setting_value(self.settings, jid_bare, 'archive')
|
||||
ixs = sqlite.get_entries_id_of_feed(db_file, feed_id)
|
||||
ixs_invalid = {}
|
||||
for ix in ixs:
|
||||
|
@ -1358,8 +1360,8 @@ class FeedTask:
|
|||
# TODO return number of archived entries and add if statement to run archive maintainence function
|
||||
await sqlite.maintain_archive(db_file, limit)
|
||||
# await sqlite.process_invalid_entries(db_file, ixs)
|
||||
await asyncio.sleep(60 * 2)
|
||||
val = Config.get_setting_value(self, jid_bare, 'check')
|
||||
await asyncio.sleep(50)
|
||||
val = Config.get_setting_value(self.settings, jid_bare, 'check')
|
||||
await asyncio.sleep(60 * float(val))
|
||||
# Schedule to call this function again in 90 minutes
|
||||
# loop.call_at(
|
||||
|
@ -1368,18 +1370,10 @@ class FeedTask:
|
|||
# self.check_updates(jid)
|
||||
# )
|
||||
|
||||
# Consider an endless loop. See XmppPubsubTask.loop_task
|
||||
# def restart_task(self, jid_bare):
|
||||
|
||||
async def loop_task(self, jid_bare):
|
||||
await asyncio.sleep(60)
|
||||
while True:
|
||||
logger.info('Looping task "check" for JID {}'.format(jid_bare))
|
||||
print('Looping task "check" for JID {}'.format(jid_bare))
|
||||
await FeedTask.check_updates(self, jid_bare)
|
||||
await asyncio.sleep(60 * 60)
|
||||
|
||||
def restart_task(self, jid_bare):
|
||||
if jid_bare == self.boundjid.bare:
|
||||
return
|
||||
if jid_bare not in self.task_manager:
|
||||
self.task_manager[jid_bare] = {}
|
||||
logger.info('Creating new task manager for JID {}'.format(jid_bare))
|
||||
|
|
|
@ -46,9 +46,9 @@ import hashlib
|
|||
from lxml import etree, html
|
||||
import os
|
||||
import random
|
||||
import slixfeed.config as config
|
||||
import slixfeed.fetch as fetch
|
||||
from slixfeed.log import Logger
|
||||
import slixfeed.sqlite as sqlite
|
||||
import sys
|
||||
from urllib.parse import (
|
||||
parse_qs,
|
||||
|
@ -67,99 +67,6 @@ except:
|
|||
logger = Logger(__name__)
|
||||
|
||||
|
||||
class Config:
|
||||
|
||||
|
||||
def get_default_data_directory():
|
||||
if os.environ.get('HOME'):
|
||||
data_home = os.path.join(os.environ.get('HOME'), '.local', 'share')
|
||||
return os.path.join(data_home, 'kaikout')
|
||||
elif sys.platform == 'win32':
|
||||
data_home = os.environ.get('APPDATA')
|
||||
if data_home is None:
|
||||
return os.path.join(
|
||||
os.path.dirname(__file__) + '/kaikout_data')
|
||||
else:
|
||||
return os.path.join(os.path.dirname(__file__) + '/kaikout_data')
|
||||
|
||||
|
||||
def get_default_config_directory():
|
||||
"""
|
||||
Determine the directory path where configuration will be stored.
|
||||
|
||||
* If $XDG_CONFIG_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to configuration directory.
|
||||
"""
|
||||
# config_home = xdg.BaseDirectory.xdg_config_home
|
||||
config_home = os.environ.get('XDG_CONFIG_HOME')
|
||||
if config_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
config_home = os.environ.get('APPDATA')
|
||||
if config_home is None:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
config_home = os.path.join(
|
||||
os.environ.get('HOME'), '.config'
|
||||
)
|
||||
return os.path.join(config_home, 'kaikout')
|
||||
|
||||
|
||||
def get_setting_value(db_file, key):
|
||||
value = sqlite.get_setting_value(db_file, key)
|
||||
if value:
|
||||
value = value[0]
|
||||
else:
|
||||
value = Config.get_value('settings', 'Settings', key)
|
||||
return value
|
||||
|
||||
|
||||
def get_values(filename, key=None):
|
||||
config_dir = Config.get_default_config_directory()
|
||||
if not os.path.isdir(config_dir):
|
||||
config_dir = '/usr/share/slixfeed/'
|
||||
if not os.path.isdir(config_dir):
|
||||
config_dir = os.path.dirname(__file__) + "/assets"
|
||||
config_file = os.path.join(config_dir, filename)
|
||||
with open(config_file, mode="rb") as defaults:
|
||||
result = tomllib.load(defaults)
|
||||
values = result[key] if key else result
|
||||
return values
|
||||
|
||||
|
||||
class Database:
|
||||
|
||||
|
||||
def instantiate(dir_data, jid_bare):
|
||||
"""
|
||||
Instantiate action on database and return its filename location.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dir_data : str
|
||||
Directory.
|
||||
jid_file : str
|
||||
Jabber ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
db_file
|
||||
Filename.
|
||||
"""
|
||||
db_file = os.path.join(dir_data, 'sqlite', f'{jid_bare}.db')
|
||||
sqlite.create_tables(db_file)
|
||||
return db_file
|
||||
|
||||
|
||||
class DateAndTime:
|
||||
|
||||
#https://feedparser.readthedocs.io/en/latest/date-parsing.html
|
||||
|
@ -183,12 +90,6 @@ class DateAndTime:
|
|||
return date
|
||||
|
||||
|
||||
def convert_seconds_to_yyyy_mm_dd(seconds_time):
|
||||
date_time = datetime.fromtimestamp(seconds_time)
|
||||
formatted_date = date_time.strftime('%Y-%m-%d')
|
||||
return formatted_date
|
||||
|
||||
|
||||
def current_date():
|
||||
"""
|
||||
Print MM DD, YYYY (Weekday Time) timestamp.
|
||||
|
@ -278,11 +179,11 @@ class DateAndTime:
|
|||
class Documentation:
|
||||
|
||||
|
||||
def manual(config_dir, section=None, command=None):
|
||||
def manual(filename, section=None, command=None):
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: filename: {}'.format(function_name, config_dir))
|
||||
filename = os.path.join(config_dir, 'commands.toml')
|
||||
with open(filename, mode="rb") as commands:
|
||||
logger.debug('{}: filename: {}'.format(function_name, filename))
|
||||
config_dir = config.get_default_config_directory()
|
||||
with open(config_dir + '/' + filename, mode="rb") as commands:
|
||||
cmds = tomllib.load(commands)
|
||||
if section == 'all':
|
||||
cmd_list = ''
|
||||
|
@ -316,7 +217,7 @@ class Html:
|
|||
async def extract_image_from_html(url):
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: url: {}'.format(function_name, url))
|
||||
result = await fetch.http(settings_network, url)
|
||||
result = await fetch.http(url)
|
||||
if not result['error']:
|
||||
data = result['content']
|
||||
tree = html.fromstring(data)
|
||||
|
@ -325,7 +226,6 @@ class Html:
|
|||
'//img[not('
|
||||
'contains(@src, "avatar") or '
|
||||
'contains(@src, "cc-by-sa") or '
|
||||
'contains(@src, "data:image/") or '
|
||||
'contains(@src, "emoji") or '
|
||||
'contains(@src, "icon") or '
|
||||
'contains(@src, "logo") or '
|
||||
|
@ -442,19 +342,6 @@ class Task:
|
|||
.format(task, jid_bare))
|
||||
|
||||
|
||||
class Toml:
|
||||
|
||||
def open_file(filename: str) -> dict:
|
||||
with open(filename, mode="rb") as fn:
|
||||
data = tomllib.load(fn)
|
||||
return data
|
||||
|
||||
def save_file(filename: str, data: dict) -> None:
|
||||
with open(filename, 'w') as fn:
|
||||
data_as_string = tomli_w.dumps(data)
|
||||
fn.write(data_as_string)
|
||||
|
||||
|
||||
"""
|
||||
|
||||
FIXME
|
||||
|
@ -491,23 +378,21 @@ class Url:
|
|||
return hostname
|
||||
|
||||
|
||||
async def replace_hostname(configuration_directory, proxies, settings_network, url, url_type):
|
||||
async def replace_hostname(url, url_type):
|
||||
"""
|
||||
Replace hostname.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proxies : list
|
||||
A list of hostnames.
|
||||
url : str
|
||||
A URL.
|
||||
URL.
|
||||
url_type : str
|
||||
A "feed" or a "link".
|
||||
"feed" or "link".
|
||||
|
||||
Returns
|
||||
-------
|
||||
url : str
|
||||
A processed URL.
|
||||
URL.
|
||||
"""
|
||||
url_new = None
|
||||
parted_url = urlsplit(url)
|
||||
|
@ -517,6 +402,7 @@ class Url:
|
|||
pathname = parted_url.path
|
||||
queries = parted_url.query
|
||||
fragment = parted_url.fragment
|
||||
proxies = config.open_config_file('proxies.toml')['proxies']
|
||||
for proxy_name in proxies:
|
||||
proxy = proxies[proxy_name]
|
||||
if hostname in proxy['hostname'] and url_type in proxy['type']:
|
||||
|
@ -536,21 +422,26 @@ class Url:
|
|||
print(proxy_url)
|
||||
print(url_new)
|
||||
print('>>>')
|
||||
response = await fetch.http(settings_network, url_new)
|
||||
response = await fetch.http(url_new)
|
||||
if (response and
|
||||
response['status_code'] == 200 and
|
||||
# response.reason == 'OK' and
|
||||
url_new.startswith(proxy_url)): break
|
||||
url_new.startswith(proxy_url)):
|
||||
break
|
||||
else:
|
||||
proxies_obsolete_file = os.path.join(configuration_directory, 'proxies_obsolete.toml')
|
||||
proxies_file = os.path.join(configuration_directory, 'proxies.toml')
|
||||
breakpoint()
|
||||
proxies_obsolete = Toml.open_file(proxies_obsolete_file)
|
||||
proxies_obsolete['proxies'][proxy_name][proxy_type].append(proxy_url)
|
||||
Toml.save_file(proxies_obsolete_file, proxies_obsolete)
|
||||
# TODO self.proxies might need to be changed, so self probably should be passed.
|
||||
proxies['proxies'][proxy_name][proxy_type].remove(proxy_url)
|
||||
Toml.save_file(proxies_file, proxies)
|
||||
config_dir = config.get_default_config_directory()
|
||||
proxies_obsolete_file = config_dir + '/proxies_obsolete.toml'
|
||||
proxies_file = config_dir + '/proxies.toml'
|
||||
if not os.path.isfile(proxies_obsolete_file):
|
||||
config.create_skeleton(proxies_file)
|
||||
config.backup_obsolete(proxies_obsolete_file,
|
||||
proxy_name, proxy_type,
|
||||
proxy_url)
|
||||
try:
|
||||
config.update_proxies(proxies_file, proxy_name,
|
||||
proxy_type, proxy_url)
|
||||
except ValueError as e:
|
||||
logger.error([str(e), proxy_url])
|
||||
url_new = None
|
||||
else:
|
||||
logger.warning('No proxy URLs for {}. '
|
||||
|
@ -561,21 +452,19 @@ class Url:
|
|||
return url_new
|
||||
|
||||
|
||||
def remove_tracking_parameters(trackers, url):
|
||||
def remove_tracking_parameters(url):
|
||||
"""
|
||||
Remove queries with tracking parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
trackers : list
|
||||
A list of queries.
|
||||
url : str
|
||||
A URL.
|
||||
URL.
|
||||
|
||||
Returns
|
||||
-------
|
||||
url : str
|
||||
A processed URL.
|
||||
URL.
|
||||
"""
|
||||
if url.startswith('data:') and ';base64,' in url:
|
||||
return url
|
||||
|
@ -585,6 +474,7 @@ class Url:
|
|||
pathname = parted_url.path
|
||||
queries = parse_qs(parted_url.query)
|
||||
fragment = parted_url.fragment
|
||||
trackers = config.open_config_file('queries.toml')['trackers']
|
||||
for tracker in trackers:
|
||||
if tracker in queries: del queries[tracker]
|
||||
queries_new = urlencode(queries, doseq=True)
|
||||
|
@ -823,12 +713,12 @@ class Utilities:
|
|||
return url_digest
|
||||
|
||||
|
||||
def pick_a_feed(dir_config, lang=None):
|
||||
def pick_a_feed(lang=None):
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: lang: {}'
|
||||
.format(function_name, lang))
|
||||
filename_feeds = os.path.join(dir_config, 'feeds.toml')
|
||||
with open(filename_feeds, mode="rb") as feeds:
|
||||
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'])
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
__version__ = '0.1.107'
|
||||
__version_info__ = (0, 1, 107)
|
||||
__version__ = '0.1.85'
|
||||
__version_info__ = (0, 1, 85)
|
||||
|
|
2253
slixfeed/xmpp/adhoc.py
Normal file
2253
slixfeed/xmpp/adhoc.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -24,32 +24,22 @@ TODO
|
|||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
from random import randrange # pending_tasks: Use a list and read the first index (i.e. index 0).
|
||||
import slixfeed.config as config
|
||||
from slixfeed.config import Config
|
||||
import slixfeed.fetch as fetch
|
||||
from slixfeed.fetch import Http
|
||||
from slixfeed.log import Logger
|
||||
import slixfeed.sqlite as sqlite
|
||||
from slixfeed.syndication import FeedTask
|
||||
from slixfeed.utilities import Database, Documentation, Html, MD, Task, Url
|
||||
from slixfeed.utilities import Documentation, Html, MD, Task, Url
|
||||
from slixfeed.xmpp.commands import XmppCommands
|
||||
from slixfeed.xmpp.message import XmppMessage
|
||||
from slixfeed.xmpp.presence import XmppPresence
|
||||
from slixfeed.xmpp.status import XmppStatusTask
|
||||
from slixfeed.xmpp.upload import XmppUpload
|
||||
from slixfeed.xmpp.utilities import XmppUtilities
|
||||
from slixmpp import JID
|
||||
from slixmpp.stanza import Message
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
from slixfeed.xmpp.encryption import XmppOmemo
|
||||
except Exception as e:
|
||||
print('Encryption of type OMEMO is not enabled. Reason: ' + str(e))
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
@ -65,8 +55,7 @@ logger = Logger(__name__)
|
|||
class XmppChat:
|
||||
|
||||
|
||||
async def process_message(
|
||||
self, message: Message, allow_untrusted: bool = False) -> None:
|
||||
async def process_message(self, message):
|
||||
"""
|
||||
Process incoming message stanzas. Be aware that this also
|
||||
includes MUC messages and error messages. It is usually
|
||||
|
@ -80,70 +69,106 @@ class XmppChat:
|
|||
for stanza objects and the Message stanza to see
|
||||
how it may be used.
|
||||
"""
|
||||
message_from = message['from']
|
||||
message_type = message['type']
|
||||
if message_type in ('chat', 'groupchat', 'normal'):
|
||||
jid_bare = message_from.bare
|
||||
message_body = message['body']
|
||||
command = ' '.join(message_body.split())
|
||||
if message['type'] in ('chat', 'groupchat', 'normal'):
|
||||
jid_bare = message['from'].bare
|
||||
command = ' '.join(message['body'].split())
|
||||
command_time_start = time.time()
|
||||
|
||||
if self.omemo_present and self['xep_0384'].is_encrypted(message):
|
||||
command, omemo_decrypted = await XmppOmemo.decrypt(
|
||||
self, message)
|
||||
else:
|
||||
omemo_decrypted = None
|
||||
# if (message['type'] == 'groupchat' and
|
||||
# message['muc']['nick'] == self.alias):
|
||||
# return
|
||||
|
||||
# FIXME Code repetition. See below.
|
||||
if message_type == 'groupchat':
|
||||
alias = message['muc']['nick']
|
||||
self_alias = XmppUtilities.get_self_alias(self, jid_bare)
|
||||
# TODO Check alias by nickname associated with conference
|
||||
if message['type'] == 'groupchat':
|
||||
if (message['muc']['nick'] == self.alias):
|
||||
return
|
||||
jid_full = str(message['from'])
|
||||
if not XmppUtilities.is_moderator(self, jid_bare, jid_full):
|
||||
return
|
||||
|
||||
if (alias == self_alias or
|
||||
not XmppUtilities.is_moderator(self, jid_bare, alias) or
|
||||
(not message_body.startswith(self_alias + ' ') and
|
||||
not message_body.startswith(self_alias + ',') and
|
||||
not message_body.startswith(self_alias + ':'))):
|
||||
return
|
||||
if message['type'] == 'groupchat':
|
||||
# nick = message['from'][message['from'].index('/')+1:]
|
||||
# nick = str(message['from'])
|
||||
# nick = nick[nick.index('/')+1:]
|
||||
if (message['muc']['nick'] == self.alias or
|
||||
not message['body'].startswith('!')):
|
||||
return
|
||||
# token = await initdb(
|
||||
# jid_bare,
|
||||
# sqlite.get_setting_value,
|
||||
# 'token'
|
||||
# )
|
||||
# if token == 'accepted':
|
||||
# operator = await initdb(
|
||||
# jid_bare,
|
||||
# sqlite.get_setting_value,
|
||||
# 'masters'
|
||||
# )
|
||||
# if operator:
|
||||
# if nick not in operator:
|
||||
# return
|
||||
# approved = False
|
||||
jid_full = str(message['from'])
|
||||
if not XmppUtilities.is_moderator(self, jid_bare, jid_full):
|
||||
return
|
||||
# if role == 'moderator':
|
||||
# approved = True
|
||||
# TODO Implement a list of temporary operators
|
||||
# Once an operator is appointed, the control would last
|
||||
# untile the participant has been disconnected from MUC
|
||||
# An operator is a function to appoint non moderators.
|
||||
# Changing nickname is fine and consist of no problem.
|
||||
# if not approved:
|
||||
# operator = await initdb(
|
||||
# jid_bare,
|
||||
# sqlite.get_setting_value,
|
||||
# 'masters'
|
||||
# )
|
||||
# if operator:
|
||||
# if nick in operator:
|
||||
# approved = True
|
||||
# if not approved:
|
||||
# return
|
||||
|
||||
# # Begin processing new JID
|
||||
# # Deprecated in favour of event 'presence_available'
|
||||
# db_dir = config.get_default_data_directory()
|
||||
# os.chdir(db_dir)
|
||||
# if jid + '.db' not in os.listdir():
|
||||
# await task_jid(jid)
|
||||
|
||||
# Adding one to the length because of
|
||||
# assumption that a comma or a dot is added
|
||||
self_alias_length = len(self_alias) + 1
|
||||
command = command[self_alias_length:].lstrip()
|
||||
# await compose.message(self, jid_bare, message)
|
||||
|
||||
if isinstance(command, Message): command = command['body']
|
||||
if message['type'] == 'groupchat':
|
||||
command = command[1:]
|
||||
command_lowercase = command.lower()
|
||||
# This is a work-around to empty messages that are caused by function
|
||||
# self.register_handler(CoroutineCallback( of module client.py.
|
||||
# The code was taken from the cho bot xample of slixmpp-omemo.
|
||||
#if not command_lowercase: return
|
||||
|
||||
logger.debug([message_from.full, ':', command])
|
||||
logger.debug([str(message['from']), ':', command])
|
||||
|
||||
# Support private message via groupchat
|
||||
# See https://codeberg.org/poezio/slixmpp/issues/3506
|
||||
if message_type == 'chat' and message.get_plugin('muc', check=True):
|
||||
# jid_bare = message_from.bare
|
||||
jid_full = message_from.full
|
||||
if message['type'] == 'chat' and message.get_plugin('muc', check=True):
|
||||
# jid_bare = message['from'].bare
|
||||
jid_full = str(message['from'])
|
||||
if (jid_bare == jid_full[:jid_full.index('/')]):
|
||||
# TODO Count and alert of MUC-PM attempts
|
||||
return
|
||||
|
||||
response = None
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
match command_lowercase:
|
||||
case 'help':
|
||||
|
||||
command_list = XmppCommands.print_help(self.dir_config)
|
||||
command_list = XmppCommands.print_help()
|
||||
response = ('Available command keys:\n'
|
||||
f'```\n{command_list}\n```\n'
|
||||
'Usage: `help <key>`')
|
||||
'```\n{}\n```\n'
|
||||
'Usage: `help <key>`'
|
||||
.format(command_list))
|
||||
case 'help all':
|
||||
command_list = Documentation.manual(
|
||||
self.dir_config, section='all')
|
||||
command_list = Documentation.manual('commands.toml', section='all')
|
||||
response = ('Complete list of commands:\n'
|
||||
f'```\n{command_list}\n```'
|
||||
.format())
|
||||
'```\n{}\n```'
|
||||
.format(command_list))
|
||||
case _ if command_lowercase.startswith('help'):
|
||||
command = command[5:].lower()
|
||||
command = command.split(' ')
|
||||
|
@ -151,64 +176,72 @@ class XmppChat:
|
|||
command_root = command[0]
|
||||
command_name = command[1]
|
||||
command_list = Documentation.manual(
|
||||
self.dir_config, section=command_root,
|
||||
command=command_name)
|
||||
'commands.toml', section=command_root, command=command_name)
|
||||
if command_list:
|
||||
command_list = ''.join(command_list)
|
||||
response = (command_list)
|
||||
else:
|
||||
response = f'KeyError for {command_root} {command_name}'
|
||||
response = ('KeyError for {} {}'
|
||||
.format(command_root, command_name))
|
||||
elif len(command) == 1:
|
||||
command = command[0]
|
||||
command_list = Documentation.manual(
|
||||
self.dir_config, command)
|
||||
command_list = Documentation.manual('commands.toml', command)
|
||||
if command_list:
|
||||
command_list = ' '.join(command_list)
|
||||
response = (f'Available command `{command}` keys:\n'
|
||||
f'```\n{command_list}\n```\n'
|
||||
f'Usage: `help {command} <command>`')
|
||||
response = ('Available command `{}` keys:\n'
|
||||
'```\n{}\n```\n'
|
||||
'Usage: `help {} <command>`'
|
||||
.format(command, command_list, command))
|
||||
else:
|
||||
response = f'KeyError for {command}'
|
||||
response = 'KeyError for {}'.format(command)
|
||||
else:
|
||||
response = ('Invalid. Enter command key '
|
||||
'or command key & name')
|
||||
case 'info':
|
||||
entries = XmppCommands.print_info_list(self)
|
||||
entries = XmppCommands.print_info_list()
|
||||
response = ('Available command options:\n'
|
||||
f'```\n{entries}\n```\n'
|
||||
'Usage: `info <option>`')
|
||||
'```\n{}\n```\n'
|
||||
'Usage: `info <option>`'
|
||||
.format(entries))
|
||||
case _ if command_lowercase.startswith('info'):
|
||||
entry = command[5:].lower()
|
||||
response = XmppCommands.print_info_specific(self, entry)
|
||||
response = XmppCommands.print_info_specific(entry)
|
||||
case _ if command_lowercase in ['greetings', 'hallo', 'hello',
|
||||
'hey', 'hi', 'hola', 'holla',
|
||||
'hollo']:
|
||||
response = (f'Greeting. My name is {self.alias}.\n'
|
||||
'I am an Atom/RSS News Bot.\n'
|
||||
'Send "help" for further instructions.\n')
|
||||
response = ('Greeting! My name is {}.\n'
|
||||
'I am an RSS News Bot.\n'
|
||||
'Send "help" for further instructions.\n'
|
||||
.format(self.alias))
|
||||
case _ if command_lowercase.startswith('add'):
|
||||
command = command[4:]
|
||||
url = command.split(' ')[0]
|
||||
title = ' '.join(command.split(' ')[1:])
|
||||
response = await XmppCommands.feed_add(
|
||||
response = XmppCommands.feed_add(
|
||||
url, db_file, jid_bare, title)
|
||||
case _ if command_lowercase.startswith('allow +'):
|
||||
val = command[7:]
|
||||
if val:
|
||||
await XmppCommands.set_filter_allow(
|
||||
db_file, val, True)
|
||||
response = f'Approved keywords\n```\n{val}\n```'
|
||||
response = ('Approved keywords\n'
|
||||
'```\n{}\n```'
|
||||
.format(val))
|
||||
else:
|
||||
response = ('No action has been taken.\n'
|
||||
response = ('No action has been taken.'
|
||||
'\n'
|
||||
'Missing keywords.')
|
||||
case _ if command_lowercase.startswith('allow -'):
|
||||
val = command[7:]
|
||||
if val:
|
||||
await XmppCommands.set_filter_allow(
|
||||
db_file, val, False)
|
||||
response = f'Approved keywords\n```\n{val}\n```'
|
||||
response = ('Approved keywords\n'
|
||||
'```\n{}\n```'
|
||||
.format(val))
|
||||
else:
|
||||
response = ('No action has been taken.\n'
|
||||
response = ('No action has been taken.'
|
||||
'\n'
|
||||
'Missing keywords.')
|
||||
case _ if command_lowercase.startswith('archive'):
|
||||
val = command[8:]
|
||||
|
@ -254,18 +287,24 @@ class XmppChat:
|
|||
if val:
|
||||
await XmppCommands.set_filter_allow(
|
||||
db_file, val, True)
|
||||
response = f'Rejected keywords\n```\n{val}\n```'
|
||||
response = ('Rejected keywords\n'
|
||||
'```\n{}\n```'
|
||||
.format(val))
|
||||
else:
|
||||
response = ('No action has been taken.\n'
|
||||
response = ('No action has been taken.'
|
||||
'\n'
|
||||
'Missing keywords.')
|
||||
case _ if command_lowercase.startswith('deny -'):
|
||||
val = command[6:]
|
||||
if val:
|
||||
await XmppCommands.set_filter_allow(
|
||||
db_file, val, False)
|
||||
response = f'Rejected keywords\n```\n{val}\n```'
|
||||
response = ('Rejected keywords\n'
|
||||
'```\n{}\n```'
|
||||
.format(val))
|
||||
else:
|
||||
response = ('No action has been taken.\n'
|
||||
response = ('No action has been taken.'
|
||||
'\n'
|
||||
'Missing keywords.')
|
||||
case _ if command_lowercase.startswith('disable'):
|
||||
response = await XmppCommands.feed_disable(
|
||||
|
@ -279,7 +318,8 @@ class XmppChat:
|
|||
if ext in ('md', 'opml'): # html xbel
|
||||
status_type = 'dnd'
|
||||
status_message = ('📤️ Procesing request to '
|
||||
f'export feeds into {ext.upper()}...')
|
||||
'export feeds into {}...'
|
||||
.format(ext.upper()))
|
||||
# pending_tasks_num = len(self.pending_tasks[jid_bare])
|
||||
pending_tasks_num = randrange(10000, 99999)
|
||||
self.pending_tasks[jid_bare][pending_tasks_num] = status_message
|
||||
|
@ -287,25 +327,15 @@ class XmppChat:
|
|||
# self.pending_tasks[jid_bare][self.pending_tasks_counter] = status_message
|
||||
XmppPresence.send(self, jid_bare, status_message,
|
||||
status_type=status_type)
|
||||
pathname, response = XmppCommands.export_feeds(
|
||||
self.dir_data, self.dir_cache, jid_bare, ext)
|
||||
encrypt_omemo = Config.get_setting_value(self, jid_bare, 'omemo')
|
||||
encrypted = True if encrypt_omemo else False
|
||||
url = await XmppUpload.start(self, jid_bare, Path(pathname), encrypted=encrypted)
|
||||
filename, response = XmppCommands.export_feeds(
|
||||
jid_bare, ext)
|
||||
url = await XmppUpload.start(self, jid_bare, filename)
|
||||
# response = (
|
||||
# f'Feeds exported successfully to {ex}.\n{url}'
|
||||
# )
|
||||
# 'Feeds exported successfully to {}.\n{}'
|
||||
# ).format(ex, url)
|
||||
# XmppMessage.send_oob_reply_message(message, url, response)
|
||||
if url:
|
||||
chat_type = await XmppUtilities.get_chat_type(self, jid_bare)
|
||||
if self.omemo_present and encrypted:
|
||||
url_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
|
||||
self, message_from, 'chat', url)
|
||||
XmppMessage.send_omemo_oob(self, message_from, url_encrypted, chat_type)
|
||||
else:
|
||||
XmppMessage.send_oob(self, jid_bare, url, chat_type)
|
||||
else:
|
||||
response = 'OPML file export has been failed.'
|
||||
chat_type = await XmppUtilities.get_chat_type(self, jid_bare)
|
||||
XmppMessage.send_oob(self, jid_bare, url, chat_type)
|
||||
del self.pending_tasks[jid_bare][pending_tasks_num]
|
||||
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
|
||||
XmppStatusTask.restart_task(self, jid_bare)
|
||||
|
@ -314,16 +344,16 @@ class XmppChat:
|
|||
'Try: md or opml')
|
||||
case _ if command_lowercase.startswith('feeds'):
|
||||
query = command[6:]
|
||||
result, number = XmppCommands.list_feeds(self.dir_config, db_file, query)
|
||||
result, number = XmppCommands.list_feeds(db_file, query)
|
||||
if number:
|
||||
if query:
|
||||
first_line = f'Subscriptions containing "{query}":\n\n```\n'
|
||||
first_line = 'Subscriptions containing "{}":\n\n```\n'.format(query)
|
||||
else:
|
||||
first_line = 'Subscriptions:\n\n```\n'
|
||||
response = (first_line + result +
|
||||
f'\n```\nTotal of {number} feeds')
|
||||
'\n```\nTotal of {} feeds'.format(number))
|
||||
case 'goodbye':
|
||||
if message_type == 'groupchat':
|
||||
if message['type'] == 'groupchat':
|
||||
await XmppCommands.muc_leave(self, jid_bare)
|
||||
else:
|
||||
response = 'This command is valid in groupchat only.'
|
||||
|
@ -348,18 +378,18 @@ class XmppChat:
|
|||
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
|
||||
XmppStatusTask.restart_task(self, jid_bare)
|
||||
case _ if command_lowercase.startswith('pubsub list'):
|
||||
jid_full_pubsub = command[12:]
|
||||
response = f'List of nodes for {jid_full_pubsub}:\n```\n'
|
||||
response = await XmppCommands.pubsub_list(self, jid_full_pubsub)
|
||||
jid = command[12:]
|
||||
response = 'List of nodes for {}:\n```\n'.format(jid)
|
||||
response = await XmppCommands.pubsub_list(self, jid)
|
||||
response += '```'
|
||||
case _ if command_lowercase.startswith('pubsub send'):
|
||||
if XmppUtilities.is_operator(self, jid_bare):
|
||||
info = command[12:]
|
||||
info = info.split(' ')
|
||||
jid_full_pubsub = info[0]
|
||||
jid = info[0]
|
||||
# num = int(info[1])
|
||||
if jid_full_pubsub:
|
||||
response = XmppCommands.pubsub_send(self, info, jid_full_pubsub)
|
||||
if jid:
|
||||
response = XmppCommands.pubsub_send(self, info, jid)
|
||||
else:
|
||||
response = ('This action is restricted. '
|
||||
'Type: sending news to PubSub.')
|
||||
|
@ -372,7 +402,8 @@ class XmppChat:
|
|||
command_lowercase.startswith('rss:/')):
|
||||
url = command
|
||||
status_type = 'dnd'
|
||||
status_message = f'📫️ Processing request to fetch data from {url}'
|
||||
status_message = ('📫️ Processing request to fetch data from {}'
|
||||
.format(url))
|
||||
# pending_tasks_num = len(self.pending_tasks[jid_bare])
|
||||
pending_tasks_num = randrange(10000, 99999)
|
||||
self.pending_tasks[jid_bare][pending_tasks_num] = status_message
|
||||
|
@ -387,9 +418,9 @@ class XmppChat:
|
|||
XmppStatusTask.restart_task(self, jid_bare)
|
||||
# except:
|
||||
# response = (
|
||||
# f'> {url}\nNews source is in the process '
|
||||
# '> {}\nNews source is in the process '
|
||||
# 'of being added to the subscription '
|
||||
# 'list.'
|
||||
# 'list.'.format(url)
|
||||
# )
|
||||
case _ if command_lowercase.startswith('interval'):
|
||||
val = command[9:]
|
||||
|
@ -422,13 +453,6 @@ class XmppChat:
|
|||
self, jid_bare, db_file)
|
||||
case _ if command_lowercase.startswith('next'):
|
||||
num = command[5:]
|
||||
if num:
|
||||
try:
|
||||
int(num)
|
||||
except:
|
||||
# NOTE Show this text as a status message
|
||||
# response = 'Argument for command "next" must be an integer.'
|
||||
num = None
|
||||
await XmppChatAction.send_unread_items(self, jid_bare, num)
|
||||
XmppStatusTask.restart_task(self, jid_bare)
|
||||
case _ if command_lowercase.startswith('node delete'):
|
||||
|
@ -450,12 +474,6 @@ class XmppChat:
|
|||
case 'old':
|
||||
response = await XmppCommands.set_old_on(
|
||||
self, jid_bare, db_file)
|
||||
case 'omemo off':
|
||||
response = await XmppCommands.set_omemo_off(
|
||||
self, jid_bare, db_file)
|
||||
case 'omemo on':
|
||||
response = await XmppCommands.set_omemo_on(
|
||||
self, jid_bare, db_file)
|
||||
case 'options':
|
||||
response = 'Options:\n```'
|
||||
response += XmppCommands.print_options(self, jid_bare)
|
||||
|
@ -478,7 +496,7 @@ class XmppChat:
|
|||
Task.stop(self, jid_bare, 'status')
|
||||
status_type = 'dnd'
|
||||
status_message = ('📫️ Processing request to fetch data '
|
||||
f'from {url}')
|
||||
'from {}'.format(url))
|
||||
pending_tasks_num = randrange(10000, 99999)
|
||||
self.pending_tasks[jid_bare][pending_tasks_num] = status_message
|
||||
response = await XmppCommands.feed_read(
|
||||
|
@ -494,7 +512,7 @@ class XmppChat:
|
|||
if not num: num = 5
|
||||
count, result = XmppCommands.print_recent(self, db_file, num)
|
||||
if count:
|
||||
response = f'Recent {num} fetched titles:\n\n```'
|
||||
response = 'Recent {} fetched titles:\n\n```'.format(num)
|
||||
response += result + '```\n'
|
||||
else:
|
||||
response = result
|
||||
|
@ -528,53 +546,15 @@ class XmppChat:
|
|||
case _ if command_lowercase.startswith('search'):
|
||||
query = command[7:]
|
||||
response = XmppCommands.search_items(db_file, query)
|
||||
case _ if command_lowercase.startswith('blacklist'):
|
||||
if XmppUtilities.is_operator(self, jid_bare):
|
||||
action_jid = command[9:].strip()
|
||||
action_jid_split = action_jid.split(' ')
|
||||
if len(action_jid_split) == 2:
|
||||
action, jid = action_jid_split
|
||||
if jid and action == 'add':
|
||||
response = XmppCommands.add_jid_to_selector(self, jid, 'blacklist')
|
||||
elif jid and action == 'delete':
|
||||
response = XmppCommands.del_jid_from_selector(self, jid, 'blacklist')
|
||||
else:
|
||||
response = f'Unknown action {action}.'
|
||||
elif len(action_jid_split) > 2:
|
||||
response = 'USAGE: blacklist <action> <jid>'
|
||||
else:
|
||||
response = XmppCommands.print_selector(self.blacklist)
|
||||
else:
|
||||
response = ('This action is restricted. '
|
||||
'Type: managing blacklist.')
|
||||
case _ if command_lowercase.startswith('whitelist'):
|
||||
if XmppUtilities.is_operator(self, jid_bare):
|
||||
action_jid = command[9:].strip()
|
||||
action_jid_split = action_jid.split(' ')
|
||||
if len(action_jid_split) == 2:
|
||||
action, jid = action_jid_split
|
||||
if jid and action == 'add':
|
||||
response = XmppCommands.add_jid_to_selector(self, jid, 'whitelist')
|
||||
elif jid and action == 'delete':
|
||||
response = XmppCommands.del_jid_from_selector(self, jid, 'whitelist')
|
||||
else:
|
||||
response = f'Unknown action {action}.'
|
||||
elif len(action_jid_split) > 2:
|
||||
response = 'USAGE: whitelist <action> <jid>'
|
||||
else:
|
||||
response = XmppCommands.print_selector(self.whitelist)
|
||||
else:
|
||||
response = ('This action is restricted. '
|
||||
'Type: managing blacklist.')
|
||||
case 'start':
|
||||
status_type = 'available'
|
||||
status_message = '📫️ Welcome back.'
|
||||
status_message = '📫️ Welcome back!'
|
||||
XmppPresence.send(self, jid_bare, status_message,
|
||||
status_type=status_type)
|
||||
await asyncio.sleep(5)
|
||||
callbacks = (FeedTask, XmppChatTask, XmppStatusTask)
|
||||
tasks = (FeedTask, XmppChatTask, XmppStatusTask)
|
||||
response = await XmppCommands.scheduler_start(
|
||||
self, db_file, jid_bare, callbacks)
|
||||
self, db_file, jid_bare, tasks)
|
||||
case 'stats':
|
||||
response = XmppCommands.print_statistics(db_file)
|
||||
case 'stop':
|
||||
|
@ -601,52 +581,57 @@ class XmppChat:
|
|||
command_time_finish = time.time()
|
||||
command_time_total = command_time_finish - command_time_start
|
||||
command_time_total = round(command_time_total, 3)
|
||||
if response:
|
||||
encrypt_omemo = Config.get_setting_value(self, jid_bare, 'omemo')
|
||||
encrypted = True if encrypt_omemo else False
|
||||
if self.omemo_present and encrypted and self['xep_0384'].is_encrypted(message):
|
||||
response_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
|
||||
self, message_from, 'chat', response)
|
||||
if omemo_decrypted and omemo_encrypted:
|
||||
# message_from = message['from']
|
||||
# message_type = message['type']
|
||||
XmppMessage.send_omemo(self, message_from, message_type, response_encrypted)
|
||||
# XmppMessage.send_omemo_reply(self, message, response_encrypted)
|
||||
else:
|
||||
XmppMessage.send_reply(self, message, response)
|
||||
if Config.get_setting_value(self, jid_bare, 'finished'):
|
||||
response_finished = f'Finished. Total time: {command_time_total}s'
|
||||
if response: XmppMessage.send_reply(self, message, response)
|
||||
if Config.get_setting_value(self.settings, jid_bare, 'finished'):
|
||||
response_finished = 'Finished. Total time: {}s'.format(command_time_total)
|
||||
XmppMessage.send_reply(self, message, response_finished)
|
||||
|
||||
# if not response: response = 'EMPTY MESSAGE - ACTION ONLY'
|
||||
# data_dir = config.get_default_data_directory()
|
||||
# if not os.path.isdir(data_dir):
|
||||
# os.mkdir(data_dir)
|
||||
# if not os.path.isdir(data_dir + '/logs/'):
|
||||
# os.mkdir(data_dir + '/logs/')
|
||||
# MD.log_to_markdown(
|
||||
# dt.current_time(), os.path.join(data_dir, 'logs', jid_bare),
|
||||
# jid_bare, command)
|
||||
# MD.log_to_markdown(
|
||||
# dt.current_time(), os.path.join(data_dir, 'logs', jid_bare),
|
||||
# jid_bare, response)
|
||||
|
||||
# print(
|
||||
# 'Message : {}\n'
|
||||
# 'JID : {}\n'
|
||||
# '{}\n'
|
||||
# .format(command, jid_bare, response)
|
||||
# )
|
||||
|
||||
|
||||
class XmppChatAction:
|
||||
|
||||
|
||||
async def send_unread_items(self, jid_bare, num: Optional[int] = None):
|
||||
async def send_unread_items(self, jid_bare, num=None):
|
||||
"""
|
||||
Send news items as messages.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jid_bare : str
|
||||
jid : str
|
||||
Jabber ID.
|
||||
num : str, optional
|
||||
Number. The default is None.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug(f'{function_name}: jid: {jid_bare} num: {num}')
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
encrypt_omemo = Config.get_setting_value(self, jid_bare, 'omemo')
|
||||
encrypted = True if encrypt_omemo else False
|
||||
jid = JID(jid_bare)
|
||||
show_media = Config.get_setting_value(self, jid_bare, 'media')
|
||||
logger.debug('{}: jid: {} num: {}'.format(function_name, jid_bare, num))
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
show_media = Config.get_setting_value(self.settings, jid_bare, 'media')
|
||||
if not num:
|
||||
num = Config.get_setting_value(self, jid_bare, 'quantum')
|
||||
num = Config.get_setting_value(self.settings, jid_bare, 'quantum')
|
||||
else:
|
||||
num = int(num)
|
||||
results = sqlite.get_unread_entries(db_file, num)
|
||||
news_digest = ''
|
||||
media_url = None
|
||||
media = None
|
||||
chat_type = await XmppUtilities.get_chat_type(self, jid_bare)
|
||||
for result in results:
|
||||
ix = result[0]
|
||||
|
@ -659,8 +644,7 @@ class XmppChatAction:
|
|||
if enclosure: enclosure = enclosure[0]
|
||||
title_f = sqlite.get_feed_title(db_file, feed_id)
|
||||
title_f = title_f[0]
|
||||
news_digest += await XmppChatAction.list_unread_entries(
|
||||
self, result, title_f, jid_bare)
|
||||
news_digest += await XmppChatAction.list_unread_entries(self, result, title_f, jid_bare)
|
||||
# print(db_file)
|
||||
# print(result[0])
|
||||
# breakpoint()
|
||||
|
@ -674,96 +658,20 @@ class XmppChatAction:
|
|||
# elif enclosure:
|
||||
if show_media:
|
||||
if enclosure:
|
||||
media_url = enclosure
|
||||
media = enclosure
|
||||
else:
|
||||
media_url = await Html.extract_image_from_html(self.settings_network, url)
|
||||
try:
|
||||
http_headers = await Http.fetch_headers(self.settings_network, media_url)
|
||||
if ('Content-Length' in http_headers):
|
||||
if int(http_headers['Content-Length']) < 100000:
|
||||
media_url = None
|
||||
else:
|
||||
media_url = None
|
||||
except Exception as e:
|
||||
print(media_url)
|
||||
logger.error(e)
|
||||
media_url = None
|
||||
|
||||
if media_url and news_digest:
|
||||
if self.omemo_present and encrypt_omemo:
|
||||
news_digest_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
|
||||
self, jid, 'chat', news_digest)
|
||||
if self.omemo_present and encrypt_omemo and omemo_encrypted:
|
||||
XmppMessage.send_omemo(
|
||||
self, jid, chat_type, news_digest_encrypted)
|
||||
else:
|
||||
# Send textual message
|
||||
XmppMessage.send(self, jid_bare, news_digest, chat_type)
|
||||
media = await Html.extract_image_from_html(url)
|
||||
|
||||
if media and news_digest:
|
||||
# Send textual message
|
||||
XmppMessage.send(self, jid_bare, news_digest, chat_type)
|
||||
news_digest = ''
|
||||
# Send media
|
||||
if self.omemo_present and encrypt_omemo:
|
||||
# if not media_url.startswith('data:'):
|
||||
filename = media_url.split('/').pop().split('?')[0]
|
||||
if not filename: breakpoint()
|
||||
pathname = os.path.join(self.dir_cache, filename)
|
||||
# http_response = await Http.response(media_url)
|
||||
http_headers = await Http.fetch_headers(self.settings_network, media_url)
|
||||
if ('Content-Length' in http_headers and
|
||||
int(http_headers['Content-Length']) < 3000000):
|
||||
status = await Http.fetch_media(self.settings_network, media_url, pathname)
|
||||
if status:
|
||||
filesize = os.path.getsize(pathname)
|
||||
media_url_new = await XmppUpload.start(
|
||||
self, jid_bare, Path(pathname), filesize,
|
||||
encrypted=encrypted)
|
||||
else:
|
||||
media_url_new = media_url
|
||||
else:
|
||||
media_url_new = media_url
|
||||
# else:
|
||||
# import io, base64
|
||||
# from PIL import Image
|
||||
# file_content = media_url.split(',').pop()
|
||||
# file_extension = media_url.split(';')[0].split(':').pop().split('/').pop()
|
||||
# img = Image.open(io.BytesIO(base64.decodebytes(bytes(file_content, "utf-8"))))
|
||||
# filename = 'image.' + file_extension
|
||||
# pathname = os.path.join(cache_dir, filename)
|
||||
# img.save(pathname)
|
||||
# filesize = os.path.getsize(pathname)
|
||||
# media_url_new = await XmppUpload.start(
|
||||
# self, jid_bare, Path(pathname), filesize, encrypted=encrypted)
|
||||
media_url_new_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
|
||||
self, jid, 'chat', media_url_new)
|
||||
if media_url_new_encrypted and omemo_encrypted:
|
||||
# NOTE Tested against Gajim.
|
||||
# FIXME This only works with aesgcm URLs, and it does
|
||||
# not work with http URLs.
|
||||
# url = saxutils.escape(url)
|
||||
# AttributeError: 'Encrypted' object has no attribute 'replace'
|
||||
XmppMessage.send_omemo_oob(
|
||||
self, jid, media_url_new_encrypted, chat_type)
|
||||
else:
|
||||
# NOTE Tested against Gajim.
|
||||
# FIXME Jandle data: URIs.
|
||||
if not media_url.startswith('data:'):
|
||||
http_headers = await Http.fetch_headers(self.settings_network, media_url)
|
||||
if ('Content-Length' in http_headers and
|
||||
int(http_headers['Content-Length']) > 100000):
|
||||
print(http_headers['Content-Length'])
|
||||
XmppMessage.send_oob(self, jid_bare, media_url, chat_type)
|
||||
else:
|
||||
XmppMessage.send_oob(self, jid_bare, media_url, chat_type)
|
||||
media_url = None
|
||||
|
||||
XmppMessage.send_oob(self, jid_bare, media, chat_type)
|
||||
media = None
|
||||
|
||||
if news_digest:
|
||||
if self.omemo_present and encrypt_omemo:
|
||||
news_digest_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
|
||||
self, jid, 'chat', news_digest)
|
||||
if self.omemo_present and encrypt_omemo and omemo_encrypted:
|
||||
XmppMessage.send_omemo(
|
||||
self, jid, chat_type, news_digest_encrypted)
|
||||
else:
|
||||
XmppMessage.send(self, jid_bare, news_digest, chat_type)
|
||||
XmppMessage.send(self, jid_bare, news_digest, chat_type)
|
||||
# TODO Add while loop to assure delivery.
|
||||
# print(await current_time(), ">>> ACT send_message",jid)
|
||||
# NOTE Do we need "if statement"? See NOTE at is_muc.
|
||||
|
@ -812,7 +720,8 @@ class XmppChatAction:
|
|||
|
||||
async def list_unread_entries(self, result, feed_title, jid):
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug(f'{function_name}: feed_title: {feed_title} jid: {jid}')
|
||||
logger.debug('{}: feed_title: {} jid: {}'
|
||||
.format(function_name, feed_title, jid))
|
||||
# TODO Add filtering
|
||||
# TODO Do this when entry is added to list and mark it as read
|
||||
# DONE!
|
||||
|
@ -828,7 +737,7 @@ class XmppChatAction:
|
|||
# print("accepted:", result[1])
|
||||
# results.extend([result])
|
||||
|
||||
# news_list = f'You have got {num} news items:\n'
|
||||
# news_list = "You've got {} news items:\n".format(num)
|
||||
# NOTE Why doesn't this work without list?
|
||||
# i.e. for result in results
|
||||
# for result in results.fetchall():
|
||||
|
@ -844,10 +753,10 @@ class XmppChatAction:
|
|||
# TODO Limit text length
|
||||
# summary = summary.replace("\n\n\n", "\n\n")
|
||||
summary = summary.replace('\n', ' ')
|
||||
summary = summary.replace(' ', ' ')
|
||||
summary = summary.replace(' ', ' ')
|
||||
# summary = summary.replace(' ', ' ')
|
||||
summary = ' '.join(summary.split())
|
||||
length = Config.get_setting_value(self, jid, 'length')
|
||||
length = Config.get_setting_value(self.settings, jid, 'length')
|
||||
length = int(length)
|
||||
summary = summary[:length] + " […]"
|
||||
# summary = summary.strip().split('\n')
|
||||
|
@ -856,11 +765,12 @@ class XmppChatAction:
|
|||
else:
|
||||
summary = '*** No summary ***'
|
||||
link = result[2]
|
||||
link = Url.remove_tracking_parameters(self.trackers, link)
|
||||
link = await Url.replace_hostname(self.dir_config, self.proxies, self.settings_network, link, "link") or link
|
||||
link = Url.remove_tracking_parameters(link)
|
||||
link = await Url.replace_hostname(link, "link") or link
|
||||
feed_id = result[4]
|
||||
# news_item = (f'\n{str(title)}\n{str(link)}\n{str(feed_title)} [{str(ix)}]\n')
|
||||
formatting = Config.get_setting_value(self, jid, 'formatting')
|
||||
# news_item = ("\n{}\n{}\n{} [{}]\n").format(str(title), str(link),
|
||||
# str(feed_title), str(ix))
|
||||
formatting = Config.get_setting_value(self.settings, jid, 'formatting')
|
||||
news_item = formatting.format(feed_title=feed_title,
|
||||
title=title,
|
||||
summary=summary,
|
||||
|
@ -875,12 +785,11 @@ class XmppChatTask:
|
|||
|
||||
|
||||
async def task_message(self, jid_bare):
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
if jid_bare not in self.settings:
|
||||
Config.add_settings_jid(self, jid_bare, db_file)
|
||||
Config.add_settings_jid(self.settings, jid_bare, db_file)
|
||||
while True:
|
||||
update_interval = Config.get_setting_value(
|
||||
self, jid_bare, 'interval')
|
||||
update_interval = Config.get_setting_value(self.settings, jid_bare, 'interval')
|
||||
update_interval = 60 * int(update_interval)
|
||||
last_update_time = sqlite.get_last_update_time(db_file)
|
||||
if last_update_time:
|
||||
|
@ -911,12 +820,13 @@ class XmppChatTask:
|
|||
return
|
||||
if jid_bare not in self.task_manager:
|
||||
self.task_manager[jid_bare] = {}
|
||||
logger.info(f'Creating new task manager for JID {jid_bare}')
|
||||
logger.info(f'Stopping task "interval" for JID {jid_bare}')
|
||||
logger.info('Creating new task manager for JID {}'.format(jid_bare))
|
||||
logger.info('Stopping task "interval" for JID {}'.format(jid_bare))
|
||||
try:
|
||||
self.task_manager[jid_bare]['interval'].cancel()
|
||||
except:
|
||||
logger.info('No task "interval" for JID {jid_bare} (XmppChatTask.task_message)')
|
||||
logger.info('Starting tasks "interval" for JID {jid_bare}')
|
||||
logger.info('No task "interval" for JID {} (XmppChatTask.task_message)'
|
||||
.format(jid_bare))
|
||||
logger.info('Starting tasks "interval" for JID {}'.format(jid_bare))
|
||||
self.task_manager[jid_bare]['interval'] = asyncio.create_task(
|
||||
XmppChatTask.task_message(self, jid_bare))
|
||||
XmppChatTask.task_message(self, jid_bare))
|
File diff suppressed because it is too large
Load diff
|
@ -2,14 +2,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from feedparser import parse
|
||||
import os
|
||||
from random import randrange
|
||||
import slixfeed.config as config
|
||||
from slixfeed.config import Config
|
||||
import slixfeed.fetch as fetch
|
||||
from slixfeed.log import Logger
|
||||
import slixfeed.sqlite as sqlite
|
||||
from slixfeed.syndication import Feed, FeedDiscovery, Opml
|
||||
from slixfeed.utilities import Database, DateAndTime, Documentation, String, Url, Utilities
|
||||
from slixfeed.utilities import DateAndTime, Documentation, String, Url, Utilities
|
||||
from slixfeed.version import __version__
|
||||
from slixfeed.xmpp.bookmark import XmppBookmark
|
||||
from slixfeed.xmpp.muc import XmppMuc
|
||||
|
@ -18,7 +18,6 @@ from slixfeed.xmpp.presence import XmppPresence
|
|||
from slixfeed.xmpp.status import XmppStatusTask
|
||||
from slixfeed.xmpp.utilities import XmppUtilities
|
||||
import sys
|
||||
import tomli_w
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
|
@ -38,60 +37,61 @@ logger = Logger(__name__)
|
|||
class XmppCommands:
|
||||
|
||||
|
||||
def print_help(dir_config):
|
||||
result = Documentation.manual(dir_config)
|
||||
def print_help():
|
||||
result = Documentation.manual('commands.toml')
|
||||
message = '\n'.join(result)
|
||||
return message
|
||||
|
||||
|
||||
def print_help_list(dir_config):
|
||||
command_list = Documentation.manual(dir_config, section='all')
|
||||
def print_help_list():
|
||||
command_list = Documentation.manual('commands.toml', section='all')
|
||||
message = ('Complete list of commands:\n'
|
||||
f'```\n{command_list}\n```')
|
||||
'```\n{}\n```'.format(command_list))
|
||||
return message
|
||||
|
||||
|
||||
def print_help_specific(dir_config, command_root, command_name):
|
||||
command_list = Documentation.manual(dir_config,
|
||||
def print_help_specific(command_root, command_name):
|
||||
command_list = Documentation.manual('commands.toml',
|
||||
section=command_root,
|
||||
command=command_name)
|
||||
if command_list:
|
||||
command_list = ''.join(command_list)
|
||||
message = (command_list)
|
||||
else:
|
||||
message = f'KeyError for {command_root} {command_name}'
|
||||
message = 'KeyError for {} {}'.format(command_root, command_name)
|
||||
return message
|
||||
|
||||
|
||||
def print_help_key(dir_config, command):
|
||||
command_list = Documentation.manual(dir_config, command)
|
||||
def print_help_key(command):
|
||||
command_list = Documentation.manual('commands.toml', command)
|
||||
if command_list:
|
||||
command_list = ' '.join(command_list)
|
||||
message = (f'Available command `{command}` keys:\n'
|
||||
f'```\n{command_list}\n```\n'
|
||||
f'Usage: `help {command} <command>`')
|
||||
message = ('Available command `{}` keys:\n'
|
||||
'```\n{}\n```\n'
|
||||
'Usage: `help {} <command>`'
|
||||
.format(command, command_list, command))
|
||||
else:
|
||||
message = f'KeyError for {command}'
|
||||
message = 'KeyError for {}'.format(command)
|
||||
return message
|
||||
|
||||
|
||||
def print_info_list(self):
|
||||
file_info = os.path.join(self.dir_config, 'information.toml')
|
||||
with open(file_info, mode="rb") as information:
|
||||
def print_info_list():
|
||||
config_dir = config.get_default_config_directory()
|
||||
with open(config_dir + '/' + 'information.toml', mode="rb") as information:
|
||||
result = tomllib.load(information)
|
||||
message = '\n'.join(result)
|
||||
return message
|
||||
|
||||
|
||||
def print_info_specific(self, entry):
|
||||
file_info = os.path.join(self.dir_config, 'information.toml')
|
||||
with open(file_info, mode="rb") as information:
|
||||
def print_info_specific(entry):
|
||||
config_dir = config.get_default_config_directory()
|
||||
with open(config_dir + '/' + 'information.toml', mode="rb") as information:
|
||||
entries = tomllib.load(information)
|
||||
if entry in entries:
|
||||
# command_list = '\n'.join(command_list)
|
||||
message = (entries[entry]['info'])
|
||||
else:
|
||||
message = f'KeyError for {entry}'
|
||||
message = 'KeyError for {}'.format(entry)
|
||||
return message
|
||||
|
||||
|
||||
|
@ -132,7 +132,7 @@ class XmppCommands:
|
|||
identifier)
|
||||
feed_id = sqlite.get_feed_id(db_file, url)
|
||||
feed_id = feed_id[0]
|
||||
result = await fetch.http(self.settings_network, url)
|
||||
result = await fetch.http(url)
|
||||
if not result['error']:
|
||||
document = result['content']
|
||||
feed = parse(document)
|
||||
|
@ -174,8 +174,10 @@ class XmppCommands:
|
|||
# the look into function "check_updates" of module "task".
|
||||
# await action.scan(self, jid_bare, db_file, url)
|
||||
# if jid_bare not in self.settings:
|
||||
# Config.add_settings_jid(self, jid_bare, db_file)
|
||||
# old = Config.get_setting_value(self, jid_bare, 'old')
|
||||
# Config.add_settings_jid(self.settings, jid_bare,
|
||||
# db_file)
|
||||
# old = Config.get_setting_value(self.settings, jid_bare,
|
||||
# 'old')
|
||||
# if old:
|
||||
# # task.clean_tasks_xmpp_chat(self, jid_bare, ['status'])
|
||||
# # await send_status(jid)
|
||||
|
@ -185,16 +187,18 @@ class XmppCommands:
|
|||
# feed_id = feed_id[0]
|
||||
# await sqlite.mark_feed_as_read(db_file, feed_id)
|
||||
|
||||
message = (f'> {url}\n'
|
||||
message = ('> {}\n'
|
||||
'News source has been '
|
||||
'added to subscription list.')
|
||||
'added to subscription list.'
|
||||
.format(url))
|
||||
else:
|
||||
ix = exist[0]
|
||||
name = exist[1]
|
||||
message = (f'> {url}\n'
|
||||
f'News source "{name}" is already '
|
||||
message = ('> {}\n'
|
||||
'News source "{}" is already '
|
||||
'listed in the subscription list at '
|
||||
f'index {ix}')
|
||||
'index {}'
|
||||
.format(url, name, ix))
|
||||
else:
|
||||
message = ('No action has been taken. Missing URL.')
|
||||
return message
|
||||
|
@ -220,9 +224,9 @@ class XmppCommands:
|
|||
keywords = sqlite.get_filter_value(db_file, 'allow')
|
||||
if keywords: keywords = str(keywords[0])
|
||||
if axis:
|
||||
val = config.add_to_list(val, keywords)
|
||||
val = await config.add_to_list(val, keywords)
|
||||
else:
|
||||
val = config.remove_from_list(val, keywords)
|
||||
val = await config.remove_from_list(val, keywords)
|
||||
if sqlite.is_filter_key(db_file, 'allow'):
|
||||
await sqlite.update_filter_value(db_file, ['allow', val])
|
||||
else:
|
||||
|
@ -230,7 +234,8 @@ class XmppCommands:
|
|||
|
||||
|
||||
def get_archive(self, jid_bare):
|
||||
result = Config.get_setting_value(self, jid_bare, 'archive')
|
||||
result = Config.get_setting_value(
|
||||
self.settings, jid_bare, 'archive')
|
||||
message = str(result)
|
||||
return message
|
||||
|
||||
|
@ -241,11 +246,12 @@ class XmppCommands:
|
|||
if val_new > 500:
|
||||
message = 'Value may not be greater than 500.'
|
||||
else:
|
||||
val_old = Config.get_setting_value(self, jid_bare, 'archive')
|
||||
val_old = Config.get_setting_value(
|
||||
self.settings, jid_bare, 'archive')
|
||||
await Config.set_setting_value(
|
||||
self, jid_bare, db_file, 'archive', val_new)
|
||||
message = ('Maximum archived items has been set to {val_new} '
|
||||
'(was: {val_old}).')
|
||||
self.settings, jid_bare, db_file, 'archive', val_new)
|
||||
message = ('Maximum archived items has been set to {} (was: {}).'
|
||||
.format(val_new, val_old))
|
||||
except:
|
||||
message = 'No action has been taken. Enter a numeric value only.'
|
||||
return message
|
||||
|
@ -253,25 +259,28 @@ class XmppCommands:
|
|||
|
||||
async def bookmark_add(self, muc_jid):
|
||||
await XmppBookmark.add(self, jid=muc_jid)
|
||||
message = f'Groupchat {muc_jid} has been added to bookmarks.'
|
||||
message = ('Groupchat {} has been added to bookmarks.'
|
||||
.format(muc_jid))
|
||||
return message
|
||||
|
||||
|
||||
async def bookmark_del(self, muc_jid):
|
||||
await XmppBookmark.remove(self, muc_jid)
|
||||
message = f'Groupchat {muc_jid} has been removed from bookmarks.'
|
||||
message = ('Groupchat {} has been removed from bookmarks.'
|
||||
.format(muc_jid))
|
||||
return message
|
||||
|
||||
|
||||
async def restore_default(self, jid_bare, key=None):
|
||||
if key:
|
||||
self.settings[jid_bare][key] = None
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
await sqlite.delete_setting(db_file, key)
|
||||
message = f'Setting {key} has been restored to default value.'
|
||||
message = ('Setting {} has been restored to default value.'
|
||||
.format(key))
|
||||
else:
|
||||
del self.settings[jid_bare]
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
await sqlite.delete_settings(db_file)
|
||||
message = 'Default settings have been restored.'
|
||||
return message
|
||||
|
@ -279,7 +288,7 @@ class XmppCommands:
|
|||
|
||||
async def clear_filter(db_file, key):
|
||||
await sqlite.delete_filter(db_file, key)
|
||||
message = f'Filter {key} has been purged.'
|
||||
message = 'Filter {} has been purged.'.format(key)
|
||||
return message
|
||||
|
||||
|
||||
|
@ -287,11 +296,11 @@ class XmppCommands:
|
|||
conferences = await XmppBookmark.get_bookmarks(self)
|
||||
message = '\nList of groupchats:\n\n```\n'
|
||||
for conference in conferences:
|
||||
conference_name = conference['name']
|
||||
conference_jid = conference['jid']
|
||||
message += (f'Name: {conference_name}\n'
|
||||
f'Room: {conference_jid}\n\n')
|
||||
message += f'```\nTotal of {len(conferences)} groupchats.\n'
|
||||
message += ('Name: {}\n'
|
||||
'Room: {}\n'
|
||||
'\n'
|
||||
.format(conference['name'], conference['jid']))
|
||||
message += ('```\nTotal of {} groupchats.\n'.format(len(conferences)))
|
||||
return message
|
||||
|
||||
|
||||
|
@ -315,19 +324,19 @@ class XmppCommands:
|
|||
keywords = sqlite.get_filter_value(db_file, 'deny')
|
||||
if keywords: keywords = str(keywords[0])
|
||||
if axis:
|
||||
val = config.add_to_list(val, keywords)
|
||||
val = await config.add_to_list(val, keywords)
|
||||
else:
|
||||
val = config.remove_from_list(val, keywords)
|
||||
val = await config.remove_from_list(val, keywords)
|
||||
if sqlite.is_filter_key(db_file, 'deny'):
|
||||
await sqlite.update_filter_value(db_file, ['deny', val])
|
||||
else:
|
||||
await sqlite.set_filter_value(db_file, ['deny', val])
|
||||
|
||||
|
||||
def export_feeds(dir_data, dir_cache, jid_bare, ext):
|
||||
pathname = Feed.export_feeds(dir_data, dir_cache, jid_bare, ext)
|
||||
message = f'Feeds successfuly exported to {ext}.'
|
||||
return pathname, message
|
||||
def export_feeds(jid_bare, ext):
|
||||
filename = Feed.export_feeds(jid_bare, ext)
|
||||
message = 'Feeds successfuly exported to {}.'.format(ext)
|
||||
return filename, message
|
||||
|
||||
|
||||
def fetch_gemini():
|
||||
|
@ -337,10 +346,11 @@ class XmppCommands:
|
|||
|
||||
async def import_opml(self, db_file, jid_bare, command):
|
||||
url = command
|
||||
result = await fetch.http(self.settings_network, url)
|
||||
result = await fetch.http(url)
|
||||
count = await Opml.import_from_file(db_file, result)
|
||||
if count:
|
||||
message = f'Successfully imported {count} feeds.'
|
||||
message = ('Successfully imported {} feeds.'
|
||||
.format(count))
|
||||
else:
|
||||
message = 'OPML file was not imported.'
|
||||
return message
|
||||
|
@ -352,7 +362,7 @@ class XmppCommands:
|
|||
for item in iq['disco_items']:
|
||||
item_id = item['node']
|
||||
item_name = item['name']
|
||||
message += f'Name: {item_name}\nNode: {item_id}\n\n'
|
||||
message += 'Name: {}\nNode: {}\n\n'.format(item_name, item_id)
|
||||
return message
|
||||
|
||||
|
||||
|
@ -381,7 +391,7 @@ class XmppCommands:
|
|||
jid = info[0]
|
||||
if '/' not in jid:
|
||||
url = info[1]
|
||||
db_file = Database.instantiate(self.dir_data, jid)
|
||||
db_file = config.get_pathname_to_database(jid)
|
||||
if len(info) > 2:
|
||||
identifier = info[2]
|
||||
else:
|
||||
|
@ -395,7 +405,8 @@ class XmppCommands:
|
|||
break
|
||||
# task.clean_tasks_xmpp_chat(self, jid_bare, ['status'])
|
||||
status_type = 'dnd'
|
||||
status_message = '📫️ Processing request to fetch data from {url}'
|
||||
status_message = ('📫️ Processing request to fetch data from {}'
|
||||
.format(url))
|
||||
# pending_tasks_num = len(self.pending_tasks[jid_bare])
|
||||
pending_tasks_num = randrange(10000, 99999)
|
||||
self.pending_tasks[jid_bare][pending_tasks_num] = status_message
|
||||
|
@ -407,41 +418,40 @@ class XmppCommands:
|
|||
url.startswith('itpc:/') or
|
||||
url.startswith('rss:/')):
|
||||
url = Url.feed_to_http(url)
|
||||
url = (await Url.replace_hostname(self.dir_config, self.proxies, self.settings_network, url, 'feed')) or url
|
||||
url = (await Url.replace_hostname(url, 'feed')) or url
|
||||
result = await Feed.add_feed(self, jid_bare, db_file, url,
|
||||
identifier)
|
||||
if isinstance(result, list):
|
||||
results = result
|
||||
message = f'Syndication feeds found for {url}\n\n```\n'
|
||||
message = "Syndication feeds found for {}\n\n```\n".format(url)
|
||||
for result in results:
|
||||
result_name = result['name']
|
||||
result_link = result['link']
|
||||
message += ('Title : {result_name}\n'
|
||||
'Link : {result_link}\n\n')
|
||||
message += f'```\nTotal of {len(results)} feeds.'
|
||||
message += ("Title : {}\n"
|
||||
"Link : {}\n"
|
||||
"\n"
|
||||
.format(result['name'], result['link']))
|
||||
message += '```\nTotal of {} feeds.'.format(len(results))
|
||||
elif result['exist']:
|
||||
result_link = result['link']
|
||||
result_name = result['name']
|
||||
result_index = result['index']
|
||||
message = (f'> {result_link}\nNews source "{result_name}" is already '
|
||||
message = ('> {}\nNews source "{}" is already '
|
||||
'listed in the subscription list at '
|
||||
f'index {result_index}')
|
||||
'index {}'
|
||||
.format(result['link'],
|
||||
result['name'],
|
||||
result['index']))
|
||||
elif result['identifier']:
|
||||
result_link = result['link']
|
||||
result_identifier = result['identifier']
|
||||
result_index = result['index']
|
||||
message = (f'> {result_link}\nIdentifier "{result_identifier}" is already '
|
||||
f'allocated to index {result_index}')
|
||||
message = ('> {}\nIdentifier "{}" is already '
|
||||
'allocated to index {}'
|
||||
.format(result['link'],
|
||||
result['identifier'],
|
||||
result['index']))
|
||||
elif result['error']:
|
||||
result_message = result['message']
|
||||
result_code = result['code']
|
||||
message = (f'> {url}\nNo subscriptions were found. '
|
||||
f'Reason: {result_message} (status code: {result_code})')
|
||||
message = ('> {}\nNo subscriptions were found. '
|
||||
'Reason: {} (status code: {})'
|
||||
.format(url, result['message'],
|
||||
result['code']))
|
||||
else:
|
||||
result_link = result['link']
|
||||
result_name = result['name']
|
||||
message = (f'> {result_link}\nNews source "{result_name}" has been '
|
||||
'added to subscription list.')
|
||||
message = ('> {}\nNews source "{}" has been '
|
||||
'added to subscription list.'
|
||||
.format(result['link'], result['name']))
|
||||
# task.clean_tasks_xmpp_chat(self, jid_bare, ['status'])
|
||||
del self.pending_tasks[jid_bare][pending_tasks_num]
|
||||
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
|
||||
|
@ -449,9 +459,9 @@ class XmppCommands:
|
|||
XmppStatusTask.restart_task(self, jid_bare)
|
||||
# except:
|
||||
# response = (
|
||||
# f'> {url}\nNews source is in the process '
|
||||
# '> {}\nNews source is in the process '
|
||||
# 'of being added to the subscription '
|
||||
# 'list.'
|
||||
# 'list.'.format(url)
|
||||
# )
|
||||
else:
|
||||
message = ('No action has been taken.'
|
||||
|
@ -470,8 +480,7 @@ class XmppCommands:
|
|||
async def fetch_http(self, url, db_file, jid_bare):
|
||||
if url.startswith('feed:/') or url.startswith('rss:/'):
|
||||
url = Url.feed_to_http(url)
|
||||
url = (await Url.replace_hostname(
|
||||
self.dir_config, self.proxies, self.settings_network, url, 'feed') or url)
|
||||
url = (await Url.replace_hostname(url, 'feed')) or url
|
||||
counter = 0
|
||||
while True:
|
||||
identifier = String.generate_identifier(url, counter)
|
||||
|
@ -483,13 +492,15 @@ class XmppCommands:
|
|||
result = await Feed.add_feed(self, jid_bare, db_file, url, identifier)
|
||||
if isinstance(result, list):
|
||||
results = result
|
||||
message = f"Syndication feeds found for {url}\n\n```\n"
|
||||
message = ("Syndication feeds found for {}\n\n```\n"
|
||||
.format(url))
|
||||
for result in results:
|
||||
message += ("Title : {}\n"
|
||||
"Link : {}\n"
|
||||
"\n"
|
||||
.format(result['name'], result['link']))
|
||||
message += f"```\nTotal of {len(results)} feeds."
|
||||
message += ('```\nTotal of {} feeds.'
|
||||
.format(len(results)))
|
||||
elif result['exist']:
|
||||
message = ('> {}\nNews source "{}" is already '
|
||||
'listed in the subscription list at '
|
||||
|
@ -507,14 +518,14 @@ class XmppCommands:
|
|||
.format(result['link'], result['name']))
|
||||
# except:
|
||||
# response = (
|
||||
# f'> {url}\nNews source is in the process '
|
||||
# '> {}\nNews source is in the process '
|
||||
# 'of being added to the subscription '
|
||||
# 'list.'
|
||||
# 'list.'.format(url)
|
||||
# )
|
||||
return message
|
||||
|
||||
|
||||
def list_feeds(dir_config, db_file, query=None):
|
||||
def list_feeds(db_file, query=None):
|
||||
if query:
|
||||
feeds = sqlite.search_feeds(db_file, query)
|
||||
else:
|
||||
|
@ -523,12 +534,14 @@ class XmppCommands:
|
|||
message = ''
|
||||
if number:
|
||||
for id, title, url in feeds:
|
||||
message += (f'\nName : {str(title)} [{str(id)}]'
|
||||
f'\nURL : {str(url)}\n')
|
||||
message += ('\nName : {} [{}]'
|
||||
'\nURL : {}'
|
||||
'\n'
|
||||
.format(str(title), str(id), str(url)))
|
||||
elif query:
|
||||
message = f"No feeds were found for: {query}"
|
||||
message = "No feeds were found for: {}".format(query)
|
||||
else:
|
||||
url = Utilities.pick_a_feed(dir_config)
|
||||
url = Utilities.pick_a_feed()
|
||||
message = ('List of subscriptions is empty. '
|
||||
'To add a feed, send a URL.\n'
|
||||
'Featured news: *{}*\n{}'
|
||||
|
@ -537,7 +550,8 @@ class XmppCommands:
|
|||
|
||||
|
||||
def get_interval(self, jid_bare):
|
||||
result = Config.get_setting_value(self, jid_bare, 'interval')
|
||||
result = Config.get_setting_value(
|
||||
self.settings, jid_bare, 'interval')
|
||||
message = str(result)
|
||||
return message
|
||||
|
||||
|
@ -545,11 +559,12 @@ class XmppCommands:
|
|||
async def set_interval(self, db_file, jid_bare, val):
|
||||
try:
|
||||
val_new = int(val)
|
||||
val_old = Config.get_setting_value(self, jid_bare, 'interval')
|
||||
val_old = Config.get_setting_value(
|
||||
self.settings, jid_bare, 'interval')
|
||||
await Config.set_setting_value(
|
||||
self, jid_bare, db_file, 'interval', val_new)
|
||||
message = (f'Updates will be sent every {val_new} minutes '
|
||||
f'(was: {val_old}).')
|
||||
self.settings, jid_bare, db_file, 'interval', val_new)
|
||||
message = ('Updates will be sent every {} minutes '
|
||||
'(was: {}).'.format(val_new, val_old))
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
message = ('No action has been taken. Enter a numeric value only.')
|
||||
|
@ -569,19 +584,20 @@ class XmppCommands:
|
|||
result = await XmppMuc.join(self, muc_jid)
|
||||
# await XmppBookmark.add(self, jid=muc_jid)
|
||||
if result == 'ban':
|
||||
message = f'{self.alias} is banned from {muc_jid}'
|
||||
message = '{} is banned from {}'.format(self.alias, muc_jid)
|
||||
else:
|
||||
await XmppBookmark.add(self, muc_jid)
|
||||
message = f'Joined groupchat {muc_jid}'
|
||||
message = 'Joined groupchat {}'.format(muc_jid)
|
||||
else:
|
||||
message = f'> {muc_jid}\nGroupchat JID appears to be invalid.'
|
||||
message = '> {}\nGroupchat JID appears to be invalid.'.format(muc_jid)
|
||||
else:
|
||||
message = '> {}\nGroupchat JID is missing.'
|
||||
return message
|
||||
|
||||
|
||||
def get_length(self, jid_bare):
|
||||
result = Config.get_setting_value(self, jid_bare, 'length')
|
||||
result = Config.get_setting_value(
|
||||
self.settings, jid_bare, 'length')
|
||||
result = str(result)
|
||||
return result
|
||||
|
||||
|
@ -589,16 +605,18 @@ class XmppCommands:
|
|||
async def set_length(self, db_file, jid_bare, val):
|
||||
try:
|
||||
val_new = int(val)
|
||||
val_old = Config.get_setting_value(self, jid_bare, 'length')
|
||||
val_old = Config.get_setting_value(
|
||||
self.settings, jid_bare, 'length')
|
||||
await Config.set_setting_value(
|
||||
self, jid_bare, db_file, 'length', val_new)
|
||||
self.settings, jid_bare, db_file, 'length', val_new)
|
||||
if not val_new: # i.e. val_new == 0
|
||||
# TODO Add action to disable limit
|
||||
message = ('Summary length limit is disabled '
|
||||
f'(was: {val_old}).')
|
||||
'(was: {}).'.format(val_old))
|
||||
else:
|
||||
message = ('Summary maximum length is set to '
|
||||
f'{val_new} characters (was: {val_old}).')
|
||||
'{} characters (was: {}).'
|
||||
.format(val_new, val_old))
|
||||
except:
|
||||
message = ('No action has been taken.'
|
||||
'\n'
|
||||
|
@ -607,41 +625,29 @@ class XmppCommands:
|
|||
|
||||
|
||||
async def set_media_off(self, jid_bare, db_file):
|
||||
await Config.set_setting_value(self, jid_bare, db_file, 'media', 0)
|
||||
await Config.set_setting_value(self.settings, jid_bare, db_file, 'media', 0)
|
||||
message = 'Media is disabled.'
|
||||
return message
|
||||
|
||||
|
||||
async def set_media_on(self, jid_bare, db_file):
|
||||
await Config.set_setting_value(self, jid_bare, db_file, 'media', 1)
|
||||
await Config.set_setting_value(self.settings, jid_bare, db_file, 'media', 1)
|
||||
message = 'Media is enabled.'
|
||||
return message
|
||||
|
||||
|
||||
async def set_old_off(self, jid_bare, db_file):
|
||||
await Config.set_setting_value(self, jid_bare, db_file, 'old', 0)
|
||||
await Config.set_setting_value(self.settings, jid_bare, db_file, 'old', 0)
|
||||
message = 'Only new items of newly added feeds be delivered.'
|
||||
return message
|
||||
|
||||
|
||||
async def set_old_on(self, jid_bare, db_file):
|
||||
await Config.set_setting_value(self, jid_bare, db_file, 'old', 1)
|
||||
await Config.set_setting_value(self.settings, jid_bare, db_file, 'old', 1)
|
||||
message = 'All items of newly added feeds be delivered.'
|
||||
return message
|
||||
|
||||
|
||||
async def set_omemo_off(self, jid_bare, db_file):
|
||||
await Config.set_setting_value(self, jid_bare, db_file, 'omemo', 0)
|
||||
message = 'OMEMO is disabled.'
|
||||
return message
|
||||
|
||||
|
||||
async def set_omemo_on(self, jid_bare, db_file):
|
||||
await Config.set_setting_value(self, jid_bare, db_file, 'omemo', 1)
|
||||
message = 'OMEMO is enabled.'
|
||||
return message
|
||||
|
||||
|
||||
def node_delete(self, info):
|
||||
info = info.split(' ')
|
||||
if len(info) > 2:
|
||||
|
@ -649,7 +655,7 @@ class XmppCommands:
|
|||
nid = info[1]
|
||||
if jid:
|
||||
XmppPubsub.delete_node(self, jid, nid)
|
||||
message = f'Deleted node: {nid}'
|
||||
message = 'Deleted node: {}'.format(nid)
|
||||
else:
|
||||
message = 'PubSub JID is missing. Enter PubSub JID.'
|
||||
else:
|
||||
|
@ -667,7 +673,7 @@ class XmppCommands:
|
|||
nid = info[1]
|
||||
if jid:
|
||||
XmppPubsub.purge_node(self, jid, nid)
|
||||
message = f'Purged node: {nid}'
|
||||
message = 'Purged node: {}'.format(nid)
|
||||
else:
|
||||
message = 'PubSub JID is missing. Enter PubSub JID.'
|
||||
else:
|
||||
|
@ -681,8 +687,8 @@ class XmppCommands:
|
|||
def print_options(self, jid_bare):
|
||||
message = ''
|
||||
for key in self.settings[jid_bare]:
|
||||
val = Config.get_setting_value(self, jid_bare, key)
|
||||
# val = Config.get_setting_value(self, jid_bare, key)
|
||||
val = Config.get_setting_value(self.settings, jid_bare, key)
|
||||
# val = Config.get_setting_value(self.settings, jid_bare, key)
|
||||
steps = 11 - len(key)
|
||||
pulse = ''
|
||||
for step in range(steps):
|
||||
|
@ -692,7 +698,8 @@ class XmppCommands:
|
|||
|
||||
|
||||
def get_quantum(self, jid_bare):
|
||||
result = Config.get_setting_value(self, jid_bare, 'quantum')
|
||||
result = Config.get_setting_value(
|
||||
self.settings, jid_bare, 'quantum')
|
||||
message = str(result)
|
||||
return message
|
||||
|
||||
|
@ -700,14 +707,16 @@ class XmppCommands:
|
|||
async def set_quantum(self, db_file, jid_bare, val):
|
||||
try:
|
||||
val_new = int(val)
|
||||
val_old = Config.get_setting_value(self, jid_bare, 'quantum')
|
||||
val_old = Config.get_setting_value(
|
||||
self.settings, jid_bare, 'quantum')
|
||||
# response = (
|
||||
# f'Every update will contain {response} news items.'
|
||||
# )
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
await Config.set_setting_value(
|
||||
self, jid_bare, db_file, 'quantum', val_new)
|
||||
message = f'Next update will contain {val_new} news items (was: {val_old}).'
|
||||
# 'Every update will contain {} news items.'
|
||||
# ).format(response)
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
await Config.set_setting_value(self.settings, jid_bare,
|
||||
db_file, 'quantum', val_new)
|
||||
message = ('Next update will contain {} news items (was: {}).'
|
||||
.format(val_new, val_old))
|
||||
except:
|
||||
message = 'No action has been taken. Enter a numeric value only.'
|
||||
return message
|
||||
|
@ -724,12 +733,12 @@ class XmppCommands:
|
|||
async def feed_read(self, jid_bare, data, url):
|
||||
if url.startswith('feed:/') or url.startswith('rss:/'):
|
||||
url = Url.feed_to_http(url)
|
||||
url = (await Url.replace_hostname(self.dir_config, self.proxies, self.settings_network, url, 'feed')) or url
|
||||
url = (await Url.replace_hostname(url, 'feed')) or url
|
||||
match len(data):
|
||||
case 1:
|
||||
if url.startswith('http'):
|
||||
while True:
|
||||
result = await fetch.http(self.settings_network, url)
|
||||
result = await fetch.http(url)
|
||||
status = result['status_code']
|
||||
if result and not result['error']:
|
||||
document = result['content']
|
||||
|
@ -738,24 +747,28 @@ class XmppCommands:
|
|||
message = Feed.view_feed(url, feed)
|
||||
break
|
||||
else:
|
||||
result = await FeedDiscovery.probe_page(self.settings_network, self.pathnames, url, document)
|
||||
result = await FeedDiscovery.probe_page(url, document)
|
||||
if isinstance(result, list):
|
||||
results = result
|
||||
message = f"Syndication feeds found for {url}\n\n```\n"
|
||||
message = ("Syndication feeds found for {}\n\n```\n"
|
||||
.format(url))
|
||||
for result in results:
|
||||
result_name = result['name']
|
||||
result_link = result['link']
|
||||
message += ("Title : {result_name}\n"
|
||||
"Link : {result_link}\n\n")
|
||||
message += f'```\nTotal of {results} feeds.'
|
||||
message += ("Title : {}\n"
|
||||
"Link : {}\n"
|
||||
"\n"
|
||||
.format(result['name'], result['link']))
|
||||
message += ('```\nTotal of {} feeds.'
|
||||
.format(len(results)))
|
||||
break
|
||||
elif not result:
|
||||
message = f'> {url}\nNo subscriptions were found.'
|
||||
message = ('> {}\nNo subscriptions were found.'
|
||||
.format(url))
|
||||
break
|
||||
else:
|
||||
url = result['link']
|
||||
else:
|
||||
message = f'> {url}\nFailed to load URL. Reason: {status}'
|
||||
message = ('> {}\nFailed to load URL. Reason: {}'
|
||||
.format(url, status))
|
||||
break
|
||||
else:
|
||||
message = ('No action has been taken. Missing URL.')
|
||||
|
@ -763,7 +776,7 @@ class XmppCommands:
|
|||
num = data[1]
|
||||
if url.startswith('http'):
|
||||
while True:
|
||||
result = await fetch.http(self.settings_network, url)
|
||||
result = await fetch.http(url)
|
||||
if result and not result['error']:
|
||||
document = result['content']
|
||||
status = result['status_code']
|
||||
|
@ -772,25 +785,28 @@ class XmppCommands:
|
|||
message = Feed.view_entry(url, feed, num)
|
||||
break
|
||||
else:
|
||||
result = await FeedDiscovery.probe_page(
|
||||
self.settings_network, self.pathnames, url, document)
|
||||
result = await FeedDiscovery.probe_page(url, document)
|
||||
if isinstance(result, list):
|
||||
results = result
|
||||
message = f"Syndication feeds found for {url}\n\n```\n"
|
||||
message = ("Syndication feeds found for {}\n\n```\n"
|
||||
.format(url))
|
||||
for result in results:
|
||||
result_name = result['name']
|
||||
result_link = result['link']
|
||||
message += (f"Title : {result_name}\n"
|
||||
f"Link : {result_link}\\n")
|
||||
message += f'```\nTotal of {len(results)} feeds.'
|
||||
message += ("Title : {}\n"
|
||||
"Link : {}\n"
|
||||
"\n"
|
||||
.format(result['name'], result['link']))
|
||||
message += ('```\nTotal of {} feeds.'
|
||||
.format(len(results)))
|
||||
break
|
||||
elif not result:
|
||||
message = f'> {url}\nNo subscriptions were found.'
|
||||
message = ('> {}\nNo subscriptions were found.'
|
||||
.format(url))
|
||||
break
|
||||
else:
|
||||
url = result['link']
|
||||
else:
|
||||
message = f'> {url}\nFailed to load URL. Reason: {status}'
|
||||
message = ('> {}\nFailed to load URL. Reason: {}'
|
||||
.format(url, status))
|
||||
break
|
||||
else:
|
||||
message = ('No action has been taken.'
|
||||
|
@ -817,7 +833,7 @@ class XmppCommands:
|
|||
message = ''
|
||||
for i in result:
|
||||
title, url, date = i
|
||||
message += f'\n{title}\n{url}\n'
|
||||
message += ('\n{}\n{}\n'.format(title, url))
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
count = False
|
||||
|
@ -857,13 +873,13 @@ class XmppCommands:
|
|||
if len(sub_removed):
|
||||
message += '\nThe following subscriptions have been removed:\n\n'
|
||||
for url in sub_removed:
|
||||
message += f'{url}\n'
|
||||
message += '{}\n'.format(url)
|
||||
if len(url_invalid):
|
||||
urls = ', '.join(url_invalid)
|
||||
message += f'\nThe following URLs do not exist:\n\n{urls}\n'
|
||||
message += '\nThe following URLs do not exist:\n\n{}\n'.format(urls)
|
||||
if len(ixs_invalid):
|
||||
ixs = ', '.join(ixs_invalid)
|
||||
message += f'\nThe following indexes do not exist:\n\n{ixs}\n'
|
||||
message += '\nThe following indexes do not exist:\n\n{}\n'.format(ixs)
|
||||
message += '\n```'
|
||||
else:
|
||||
message = ('No action has been taken.'
|
||||
|
@ -904,13 +920,13 @@ class XmppCommands:
|
|||
if len(sub_marked):
|
||||
message += '\nThe following subscriptions have been marked as read:\n\n'
|
||||
for url in sub_marked:
|
||||
message += f'{url}\n'
|
||||
message += '{}\n'.format(url)
|
||||
if len(url_invalid):
|
||||
urls = ', '.join(url_invalid)
|
||||
message += f'\nThe following URLs do not exist:\n\n{urls}\n'
|
||||
message += '\nThe following URLs do not exist:\n\n{}\n'.format(urls)
|
||||
if len(ixs_invalid):
|
||||
ixs = ', '.join(ixs_invalid)
|
||||
message += f'\nThe following indexes do not exist:\n\n{ixs}\n'
|
||||
message += '\nThe following indexes do not exist:\n\n{}\n'.format(ixs)
|
||||
message += '\n```'
|
||||
else:
|
||||
await sqlite.mark_all_as_read(db_file)
|
||||
|
@ -922,33 +938,37 @@ class XmppCommands:
|
|||
if query:
|
||||
if len(query) > 3:
|
||||
results = sqlite.search_entries(db_file, query)
|
||||
message = f"Search results for '{query}':\n\n```"
|
||||
message = ("Search results for '{}':\n\n```"
|
||||
.format(query))
|
||||
for result in results:
|
||||
message += f'\n{str(result[0])}\n{str(result[1])}\n'
|
||||
message += ("\n{}\n{}\n"
|
||||
.format(str(result[0]), str(result[1])))
|
||||
if len(results):
|
||||
message += f'```\nTotal of {len(results)} results'
|
||||
message += "```\nTotal of {} results".format(len(results))
|
||||
else:
|
||||
message = f'No results were found for: {query}'
|
||||
message = "No results were found for: {}".format(query)
|
||||
else:
|
||||
message = 'Enter at least 4 characters to search'
|
||||
else:
|
||||
message = ('No action has been taken.\n'
|
||||
message = ('No action has been taken.'
|
||||
'\n'
|
||||
'Missing search query.')
|
||||
return message
|
||||
|
||||
|
||||
# Tasks are classes which are passed to this function
|
||||
# On an occasion in which they would have returned, variable "tasks" might be called "callback"
|
||||
async def scheduler_start(self, db_file, jid_bare, callbacks):
|
||||
await Config.set_setting_value(self, jid_bare, db_file, 'enabled', 1)
|
||||
for callback in callbacks:
|
||||
callback.restart_task(self, jid_bare)
|
||||
async def scheduler_start(self, db_file, jid_bare, tasks):
|
||||
await Config.set_setting_value(self.settings, jid_bare, db_file, 'enabled', 1)
|
||||
for task in tasks:
|
||||
task.restart_task(self, jid_bare)
|
||||
message = 'Updates are enabled.'
|
||||
return message
|
||||
|
||||
|
||||
async def scheduler_stop(self, db_file, jid_bare):
|
||||
await Config.set_setting_value(self, jid_bare, db_file, 'enabled', 0)
|
||||
await Config.set_setting_value(
|
||||
self.settings, jid_bare, db_file, 'enabled', 0)
|
||||
for task in ('interval', 'status'):
|
||||
if (jid_bare in self.task_manager and
|
||||
task in self.task_manager[jid_bare]):
|
||||
|
@ -987,9 +1007,12 @@ class XmppCommands:
|
|||
"\n"
|
||||
"```"
|
||||
"\n"
|
||||
f"News items : {entries_unread}/{entries}\n"
|
||||
f"News sources : {feeds_active}/{feeds_all}\n"
|
||||
"```")
|
||||
"News items : {}/{}\n"
|
||||
"News sources : {}/{}\n"
|
||||
"```").format(entries_unread,
|
||||
entries,
|
||||
feeds_active,
|
||||
feeds_all)
|
||||
return message
|
||||
|
||||
|
||||
|
@ -1002,9 +1025,11 @@ class XmppCommands:
|
|||
name = name[0]
|
||||
addr = sqlite.get_feed_url(db_file, feed_id)
|
||||
addr = addr[0]
|
||||
message = f'Updates are now disabled for subscription:\n{addr}\n{name}'
|
||||
message = ('Updates are now disabled for subscription:\n{}\n{}'
|
||||
.format(addr, name))
|
||||
except:
|
||||
message = f'No action has been taken. No news source with index {feed_id}.'
|
||||
message = ('No action has been taken. No news source with index {}.'
|
||||
.format(feed_id))
|
||||
XmppStatusTask.restart_task(self, jid_bare)
|
||||
return message
|
||||
|
||||
|
@ -1015,11 +1040,13 @@ class XmppCommands:
|
|||
await sqlite.set_enabled_status(db_file, feed_id, 1)
|
||||
name = sqlite.get_feed_title(db_file, feed_id)[0]
|
||||
addr = sqlite.get_feed_url(db_file, feed_id)[0]
|
||||
message = (f'> {addr}\n'
|
||||
f'Updates are now enabled for news source "{name}"')
|
||||
message = ('> {}\n'
|
||||
'Updates are now enabled for news source "{}"'
|
||||
.format(addr, name))
|
||||
except:
|
||||
message = ('No action has been taken.\n'
|
||||
f'No news source with index {feed_id}.')
|
||||
message = ('No action has been taken.'
|
||||
'\n'
|
||||
'No news source with index {}.'.format(feed_id))
|
||||
return message
|
||||
|
||||
|
||||
|
@ -1041,11 +1068,13 @@ class XmppCommands:
|
|||
else:
|
||||
await sqlite.set_feed_title(db_file, feed_id,
|
||||
name)
|
||||
message = (f'> {name_old}\n'
|
||||
f'Subscription #{feed_id} has been '
|
||||
f'renamed to "{name}".')
|
||||
message = ('> {}'
|
||||
'\n'
|
||||
'Subscription #{} has been '
|
||||
'renamed to "{}".'.format(
|
||||
name_old,feed_id, name))
|
||||
else:
|
||||
message = f'Subscription with Id {feed_id} does not exist.'
|
||||
message = 'Subscription with Id {} does not exist.'.format(feed_id)
|
||||
except:
|
||||
message = ('No action has been taken.'
|
||||
'\n'
|
||||
|
@ -1058,50 +1087,11 @@ class XmppCommands:
|
|||
return message
|
||||
|
||||
|
||||
def add_jid_to_selector(self, jid, list_type):
|
||||
filename = os.path.join(self.dir_config, 'selector.toml')
|
||||
match list_type:
|
||||
case 'blacklist':
|
||||
list_type_list = self.blacklist
|
||||
case 'whitelist':
|
||||
list_type_list = self.whitelist
|
||||
if jid in list_type_list:
|
||||
message = f'Jabber ID {jid} is already included in {list_type}.\nNo action has been committed.'
|
||||
else:
|
||||
list_type_list.append(jid)
|
||||
Config.update_toml_file(filename, self.selector)
|
||||
message = f'Jabber ID {jid} has been added to {list_type}.'
|
||||
return message
|
||||
|
||||
|
||||
def del_jid_from_selector(self, jid, list_type):
|
||||
filename = os.path.join(self.dir_config, 'selector.toml')
|
||||
match list_type:
|
||||
case 'blacklist':
|
||||
list_type_list = self.blacklist
|
||||
case 'whitelist':
|
||||
list_type_list = self.whitelist
|
||||
if jid in list_type_list:
|
||||
list_type_list.remove(jid)
|
||||
Config.update_toml_file(filename, self.selector)
|
||||
message = f'Jabber ID "{jid}" has been removed from {list_type}.'
|
||||
else:
|
||||
message = f'Jabber ID "{jid}" was not found in {list_type}.\nNo action has been committed.'
|
||||
return message
|
||||
|
||||
|
||||
def print_selector(list_type):
|
||||
jids = ' '.join(list_type) if list_type else '(empty).'
|
||||
message = f'Jabber IDs: {jids}'
|
||||
return message
|
||||
|
||||
|
||||
def print_support_jid():
|
||||
muc_jid = 'slixfeed@chat.woodpeckersnest.space'
|
||||
message = f'Join xmpp:{muc_jid}?join'
|
||||
message = 'Join xmpp:{}?join'.format(muc_jid)
|
||||
return message
|
||||
|
||||
|
||||
|
||||
async def invite_jid_to_muc(self, jid_bare):
|
||||
muc_jid = 'slixfeed@chat.woodpeckersnest.space'
|
||||
if await XmppUtilities.get_chat_type(self, jid_bare) == 'chat':
|
||||
|
|
3769
slixfeed/xmpp/component.py
Normal file
3769
slixfeed/xmpp/component.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,347 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
|
||||
TODO
|
||||
|
||||
1) Deprecate "add" (see above) and make it interactive.
|
||||
Slixfeed: Do you still want to add this URL to subscription list?
|
||||
See: case _ if command_lowercase.startswith("add"):
|
||||
|
||||
2) If subscription is inadequate (see XmppPresence.request), send a message that says so.
|
||||
|
||||
elif not self.client_roster[jid]["to"]:
|
||||
breakpoint()
|
||||
message.reply("Share online status to activate bot.").send()
|
||||
return
|
||||
|
||||
3) Set timeout for moderator interaction.
|
||||
If moderator interaction has been made, and moderator approves the bot, then
|
||||
the bot will add the given groupchat to bookmarks; otherwise, the bot will
|
||||
send a message that it was not approved and therefore leaves the groupchat.
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
from omemo.storage import Just, Maybe, Nothing, Storage
|
||||
from omemo.types import DeviceInformation, JSONType
|
||||
import os
|
||||
from slixfeed.config import Data
|
||||
from slixfeed.log import Logger
|
||||
from slixmpp import JID
|
||||
from slixmpp.exceptions import IqTimeout, IqError
|
||||
#from slixmpp.plugins import register_plugin
|
||||
from slixmpp.stanza import Message
|
||||
from slixmpp_omemo import TrustLevel, XEP_0384
|
||||
from typing import Any, Dict, FrozenSet, Literal, Optional, Union
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
# for task in main_task:
|
||||
# task.cancel()
|
||||
|
||||
# Deprecated in favour of event "presence_available"
|
||||
# if not main_task:
|
||||
# await select_file()
|
||||
|
||||
|
||||
class XmppOmemo:
|
||||
|
||||
|
||||
async def decrypt(self, stanza: Message):
|
||||
|
||||
omemo_decrypted = None
|
||||
|
||||
mto = stanza["from"]
|
||||
mtype = stanza["type"]
|
||||
|
||||
namespace = self['xep_0384'].is_encrypted(stanza)
|
||||
if namespace is None:
|
||||
omemo_decrypted = False
|
||||
response = f"Unencrypted message or unsupported message encryption: {stanza['body']}"
|
||||
else:
|
||||
print(f'Message in namespace {namespace} received: {stanza}')
|
||||
try:
|
||||
response, device_information = await self['xep_0384'].decrypt_message(stanza)
|
||||
print(f'Information about sender: {device_information}')
|
||||
omemo_decrypted = True
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
response = f'Error {type(e).__name__}: {e}'
|
||||
|
||||
return response, omemo_decrypted
|
||||
|
||||
|
||||
async def encrypt(
|
||||
self,
|
||||
mto: JID,
|
||||
mtype: Literal['chat', 'normal'],
|
||||
mbody: str
|
||||
) -> None:
|
||||
|
||||
if isinstance(mbody, str):
|
||||
reply = self.make_message(mto=mto, mtype=mtype)
|
||||
reply['body'] = mbody
|
||||
|
||||
reply.set_to(mto)
|
||||
reply.set_from(self.boundjid)
|
||||
|
||||
# It might be a good idea to strip everything except for the body from the stanza,
|
||||
# since some things might break when echoed.
|
||||
message, encryption_errors = await self['xep_0384'].encrypt_message(reply, mto)
|
||||
|
||||
if len(encryption_errors) > 0:
|
||||
print(f'There were non-critical errors during encryption: {encryption_errors}')
|
||||
#log.info(f'There were non-critical errors during encryption: {encryption_errors}')
|
||||
|
||||
# for namespace, message in messages.items():
|
||||
# message['eme']['namespace'] = namespace
|
||||
# message['eme']['name'] = self['xep_0380'].mechanisms[namespace]
|
||||
|
||||
return message, True
|
||||
|
||||
|
||||
async def _decrypt(self, message: Message, allow_untrusted: bool = False):
|
||||
jid = message['from']
|
||||
try:
|
||||
print('XmppOmemo.decrypt')
|
||||
message_omemo_encrypted = message['omemo_encrypted']
|
||||
message_body = await self['xep_0384'].decrypt_message(
|
||||
message_omemo_encrypted, jid, allow_untrusted)
|
||||
# decrypt_message returns Optional[str]. It is possible to get
|
||||
# body-less OMEMO message (see KeyTransportMessages), currently
|
||||
# used for example to send heartbeats to other devices.
|
||||
if message_body is not None:
|
||||
response = message_body.decode('utf8')
|
||||
omemo_decrypted = True
|
||||
else:
|
||||
omemo_decrypted = response = None
|
||||
retry = None
|
||||
except (MissingOwnKey,) as exn:
|
||||
print('XmppOmemo.decrypt. except: MissingOwnKey')
|
||||
# The message is missing our own key, it was not encrypted for
|
||||
# us, and we can't decrypt it.
|
||||
response = ('Error: Your message has not been encrypted for '
|
||||
'Slixfeed (MissingOwnKey).')
|
||||
omemo_decrypted = False
|
||||
retry = False
|
||||
logger.error(exn)
|
||||
except (NoAvailableSession,) as exn:
|
||||
print('XmppOmemo.decrypt. except: NoAvailableSession')
|
||||
# We received a message from that contained a session that we
|
||||
# don't know about (deleted session storage, etc.). We can't
|
||||
# decrypt the message, and it's going to be lost.
|
||||
# Here, as we need to initiate a new encrypted session, it is
|
||||
# best if we send an encrypted message directly. XXX: Is it
|
||||
# where we talk about self-healing messages?
|
||||
response = ('Error: Your message has not been encrypted for '
|
||||
'Slixfeed (NoAvailableSession).')
|
||||
omemo_decrypted = False
|
||||
retry = False
|
||||
logger.error(exn)
|
||||
except (UndecidedException, UntrustedException) as exn:
|
||||
print('XmppOmemo.decrypt. except: UndecidedException')
|
||||
print('XmppOmemo.decrypt. except: UntrustedException')
|
||||
# We received a message from an untrusted device. We can
|
||||
# choose to decrypt the message nonetheless, with the
|
||||
# `allow_untrusted` flag on the `decrypt_message` call, which
|
||||
# we will do here. This is only possible for decryption,
|
||||
# encryption will require us to decide if we trust the device
|
||||
# or not. Clients _should_ indicate that the message was not
|
||||
# trusted, or in undecided state, if they decide to decrypt it
|
||||
# anyway.
|
||||
response = (f'Error: Device "{exn.device}" is not present in the '
|
||||
'trusted devices of Slixfeed.')
|
||||
omemo_decrypted = False
|
||||
retry = True
|
||||
logger.error(exn)
|
||||
# We resend, setting the `allow_untrusted` parameter to True.
|
||||
# await XmppChat.process_message(self, message, allow_untrusted=True)
|
||||
except (EncryptionPrepareException,) as exn:
|
||||
print('XmppOmemo.decrypt. except: EncryptionPrepareException')
|
||||
# Slixmpp tried its best, but there were errors it couldn't
|
||||
# resolve. At this point you should have seen other exceptions
|
||||
# and given a chance to resolve them already.
|
||||
response = ('Error: Your message has not been encrypted for '
|
||||
'Slixfeed (EncryptionPrepareException).')
|
||||
omemo_decrypted = False
|
||||
retry = False
|
||||
logger.error(exn)
|
||||
except (Exception,) as exn:
|
||||
print('XmppOmemo.decrypt. except: Exception')
|
||||
response = ('Error: Your message has not been encrypted for '
|
||||
'Slixfeed (Unknown).')
|
||||
omemo_decrypted = False
|
||||
retry = False
|
||||
logger.error(exn)
|
||||
raise
|
||||
|
||||
return response, omemo_decrypted, retry
|
||||
|
||||
|
||||
async def _encrypt(self, jid: JID, message_body):
|
||||
print(jid)
|
||||
print(message_body)
|
||||
expect_problems = {} # type: Optional[Dict[JID, List[int]]]
|
||||
while True:
|
||||
try:
|
||||
print('XmppOmemo.encrypt')
|
||||
# `encrypt_message` excepts the plaintext to be sent, a list of
|
||||
# bare JIDs to encrypt to, and optionally a dict of problems to
|
||||
# expect per bare JID.
|
||||
#
|
||||
# Note that this function returns an `<encrypted/>` object,
|
||||
# and not a full Message stanza. This combined with the
|
||||
# `recipients` parameter that requires for a list of JIDs,
|
||||
# allows you to encrypt for 1:1 as well as groupchats (MUC).
|
||||
#
|
||||
# `expect_problems`: See EncryptionPrepareException handling.
|
||||
recipients = [jid]
|
||||
message_body = await self['xep_0384'].encrypt_message(
|
||||
message_body, recipients, expect_problems)
|
||||
omemo_encrypted = True
|
||||
break
|
||||
except UndecidedException as exn:
|
||||
print('XmppOmemo.encrypt. except: UndecidedException')
|
||||
# The library prevents us from sending a message to an
|
||||
# untrusted/undecided barejid, so we need to make a decision here.
|
||||
# This is where you prompt your user to ask what to do. In
|
||||
# this bot we will automatically trust undecided recipients.
|
||||
await self['xep_0384'].trust(exn.bare_jid, exn.device, exn.ik)
|
||||
omemo_encrypted = False
|
||||
# TODO: catch NoEligibleDevicesException
|
||||
except EncryptionPrepareException as exn:
|
||||
print('XmppOmemo.encrypt. except: EncryptionPrepareException')
|
||||
# This exception is being raised when the library has tried
|
||||
# all it could and doesn't know what to do anymore. It
|
||||
# contains a list of exceptions that the user must resolve, or
|
||||
# explicitely ignore via `expect_problems`.
|
||||
# TODO: We might need to bail out here if errors are the same?
|
||||
for error in exn.errors:
|
||||
if isinstance(error, MissingBundleException):
|
||||
# We choose to ignore MissingBundleException. It seems
|
||||
# to be somewhat accepted that it's better not to
|
||||
# encrypt for a device if it has problems and encrypt
|
||||
# for the rest, rather than error out. The "faulty"
|
||||
# device won't be able to decrypt and should display a
|
||||
# generic message. The receiving end-user at this
|
||||
# point can bring up the issue if it happens.
|
||||
message_body = (f'Could not find keys for device '
|
||||
'"{error.device}"'
|
||||
f' of recipient "{error.bare_jid}". '
|
||||
'Skipping.')
|
||||
omemo_encrypted = False
|
||||
jid = JID(error.bare_jid)
|
||||
device_list = expect_problems.setdefault(jid, [])
|
||||
device_list.append(error.device)
|
||||
except (IqError, IqTimeout) as exn:
|
||||
print('XmppOmemo.encrypt. except: IqError, IqTimeout')
|
||||
message_body = ('An error occured while fetching information '
|
||||
'on a recipient.\n%r' % exn)
|
||||
omemo_encrypted = False
|
||||
except Exception as exn:
|
||||
print('XmppOmemo.encrypt. except: Exception')
|
||||
message_body = ('An error occured while attempting to encrypt'
|
||||
'.\n%r' % exn)
|
||||
omemo_encrypted = False
|
||||
raise
|
||||
|
||||
return message_body, omemo_encrypted
|
||||
|
||||
|
||||
class StorageImpl(Storage):
|
||||
"""
|
||||
Example storage implementation that stores all data in a single JSON file.
|
||||
"""
|
||||
|
||||
dir_data = Data.get_directory()
|
||||
omemo_dir = os.path.join(dir_data, 'omemo')
|
||||
JSON_FILE = os.path.join(omemo_dir, 'omemo.json')
|
||||
|
||||
# TODO Pass JID
|
||||
#JSON_FILE = os.path.join(omemo_dir, f'{jid_bare}.json')
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.__data: Dict[str, JSONType] = {}
|
||||
try:
|
||||
with open(self.JSON_FILE, encoding="utf8") as f:
|
||||
self.__data = json.load(f)
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
pass
|
||||
|
||||
async def _load(self, key: str) -> Maybe[JSONType]:
|
||||
if key in self.__data:
|
||||
return Just(self.__data[key])
|
||||
|
||||
return Nothing()
|
||||
|
||||
async def _store(self, key: str, value: JSONType) -> None:
|
||||
self.__data[key] = value
|
||||
with open(self.JSON_FILE, "w", encoding="utf8") as f:
|
||||
json.dump(self.__data, f)
|
||||
|
||||
async def _delete(self, key: str) -> None:
|
||||
self.__data.pop(key, None)
|
||||
with open(self.JSON_FILE, "w", encoding="utf8") as f:
|
||||
json.dump(self.__data, f)
|
||||
|
||||
|
||||
class XEP_0384Impl(XEP_0384): # pylint: disable=invalid-name
|
||||
"""
|
||||
Example implementation of the OMEMO plugin for Slixmpp.
|
||||
"""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None: # pylint: disable=redefined-outer-name
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Just the type definition here
|
||||
self.__storage: Storage
|
||||
|
||||
def plugin_init(self) -> None:
|
||||
self.__storage = StorageImpl()
|
||||
|
||||
super().plugin_init()
|
||||
|
||||
@property
|
||||
def storage(self) -> Storage:
|
||||
return self.__storage
|
||||
|
||||
@property
|
||||
def _btbv_enabled(self) -> bool:
|
||||
return True
|
||||
|
||||
async def _devices_blindly_trusted(
|
||||
self,
|
||||
blindly_trusted: FrozenSet[DeviceInformation],
|
||||
identifier: Optional[str]
|
||||
) -> None:
|
||||
print(f"[{identifier}] Devices trusted blindly: {blindly_trusted}")
|
||||
#log.info(f"[{identifier}] Devices trusted blindly: {blindly_trusted}")
|
||||
|
||||
async def _prompt_manual_trust(
|
||||
self,
|
||||
manually_trusted: FrozenSet[DeviceInformation],
|
||||
identifier: Optional[str]
|
||||
) -> None:
|
||||
# Since BTBV is enabled and we don't do any manual trust adjustments in the example, this method
|
||||
# should never be called. All devices should be automatically trusted blindly by BTBV.
|
||||
|
||||
# To show how a full implementation could look like, the following code will prompt for a trust
|
||||
# decision using `input`:
|
||||
session_mananger = await self.get_session_manager()
|
||||
|
||||
for device in manually_trusted:
|
||||
while True:
|
||||
answer = input(f"[{identifier}] Trust the following device? (yes/no) {device}")
|
||||
if answer in { "yes", "no" }:
|
||||
await session_mananger.set_trust(
|
||||
device.bare_jid,
|
||||
device.identity_key,
|
||||
TrustLevel.TRUSTED.value if answer == "yes" else TrustLevel.DISTRUSTED.value
|
||||
)
|
||||
break
|
||||
print("Please answer yes or no.")
|
||||
|
||||
#register_plugin(XEP_0384Impl)
|
|
@ -34,31 +34,21 @@ class XmppGroupchat:
|
|||
'bookmark {}'.format(bookmark['name']))
|
||||
alias = bookmark["nick"]
|
||||
muc_jid = bookmark["jid"]
|
||||
# Message.printer('Joining to MUC {} ...'.format(muc_jid))
|
||||
print('Joining to MUC {} ...'.format(muc_jid))
|
||||
Message.printer('Joining to MUC {} ...'.format(muc_jid))
|
||||
result = await XmppMuc.join(self, muc_jid, alias)
|
||||
match result:
|
||||
case 'ban':
|
||||
await XmppBookmark.remove(self, muc_jid)
|
||||
logger.warning('{} is banned from {}'.format(self.alias, muc_jid))
|
||||
logger.warning('Groupchat {} has been removed from bookmarks'
|
||||
.format(muc_jid))
|
||||
case 'error':
|
||||
logger.warning('An error has occured while attempting '
|
||||
'to join to groupchat {}'
|
||||
.format(muc_jid))
|
||||
case 'timeout':
|
||||
logger.warning('Timeout has reached while attempting '
|
||||
'to join to groupchat {}'
|
||||
.format(muc_jid))
|
||||
case _:
|
||||
logger.info('Autojoin groupchat\n'
|
||||
'Name : {}\n'
|
||||
'JID : {}\n'
|
||||
'Alias : {}\n'
|
||||
.format(bookmark["name"],
|
||||
bookmark["jid"],
|
||||
bookmark["nick"]))
|
||||
if result == 'ban':
|
||||
await XmppBookmark.remove(self, muc_jid)
|
||||
logger.warning('{} is banned from {}'.format(self.alias, muc_jid))
|
||||
logger.warning('Groupchat {} has been removed from bookmarks'
|
||||
.format(muc_jid))
|
||||
else:
|
||||
logger.info('Autojoin groupchat\n'
|
||||
'Name : {}\n'
|
||||
'JID : {}\n'
|
||||
'Alias : {}\n'
|
||||
.format(bookmark["name"],
|
||||
bookmark["jid"],
|
||||
bookmark["nick"]))
|
||||
elif not bookmark["jid"]:
|
||||
logger.error('JID is missing for bookmark {}'
|
||||
.format(bookmark['name']))
|
||||
|
|
|
@ -11,8 +11,8 @@ socket (i.e. clients[fd]) from the respective client.
|
|||
|
||||
import asyncio
|
||||
import os
|
||||
import slixfeed.config as config
|
||||
from slixfeed.syndication import FeedTask
|
||||
from slixfeed.utilities import Database
|
||||
from slixfeed.xmpp.chat import XmppChatTask
|
||||
from slixfeed.xmpp.commands import XmppCommands
|
||||
from slixfeed.xmpp.chat import XmppChatAction
|
||||
|
@ -85,7 +85,7 @@ class XmppIpcServer:
|
|||
if '~' in data:
|
||||
data_list = data.split('~')
|
||||
jid_bare = data_list[0]
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
command = data_list[1]
|
||||
else:
|
||||
command = data
|
||||
|
@ -177,7 +177,7 @@ class XmppIpcServer:
|
|||
ext = command[7:]
|
||||
if ext in ('md', 'opml'):
|
||||
filename, result = XmppCommands.export_feeds(
|
||||
self.dir_data, self.dir_cache, jid_bare, ext)
|
||||
self, jid_bare, ext)
|
||||
response = result + ' : ' + filename
|
||||
else:
|
||||
response = 'Unsupported filetype. Try: md or opml'
|
||||
|
@ -204,10 +204,10 @@ class XmppIpcServer:
|
|||
response = await XmppCommands.import_opml(
|
||||
self, db_file, jid_bare, command)
|
||||
case 'info':
|
||||
response = XmppCommands.print_info_list(self)
|
||||
response = XmppCommands.print_info_list()
|
||||
case _ if command.startswith('info'):
|
||||
entry = command[5:].lower()
|
||||
response = XmppCommands.print_info_specific(self, entry)
|
||||
response = XmppCommands.print_info_specific(entry)
|
||||
case 'pubsub list':
|
||||
response = await XmppCommands.pubsub_list(
|
||||
self, jid_bare)
|
||||
|
@ -231,7 +231,7 @@ class XmppIpcServer:
|
|||
command.startswith('itpc:/') or
|
||||
command.startswith('rss:/')):
|
||||
response = await XmppCommands.fetch_http(
|
||||
self.settings_network, command, db_file, jid_bare)
|
||||
self, command, db_file, jid_bare)
|
||||
case _ if command.startswith('interval'):
|
||||
val = command[9:]
|
||||
if val:
|
||||
|
|
|
@ -10,13 +10,10 @@ class XmppIQ:
|
|||
|
||||
async def send(self, iq):
|
||||
try:
|
||||
result = await iq.send(timeout=15)
|
||||
await iq.send(timeout=15)
|
||||
except IqTimeout as e:
|
||||
logger.error('Error Timeout')
|
||||
logger.error(str(e))
|
||||
result = e
|
||||
except IqError as e:
|
||||
logger.error('Error XmppIQ')
|
||||
logger.error(str(e))
|
||||
result = e
|
||||
return result
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from slixfeed.log import Logger
|
||||
from slixmpp import JID
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
@ -40,46 +39,6 @@ class XmppMessage:
|
|||
mnick=self.alias)
|
||||
|
||||
|
||||
def send_omemo(self, jid: JID, chat_type, response_encrypted):
|
||||
# jid_from = str(self.boundjid) if self.is_component else None
|
||||
# message = self.make_message(mto=jid, mfrom=jid_from, mtype=chat_type)
|
||||
# eme_ns = 'eu.siacs.conversations.axolotl'
|
||||
# message['eme']['namespace'] = eme_ns
|
||||
# message['eme']['name'] = self['xep_0380'].mechanisms[eme_ns]
|
||||
# message['eme'] = {'name': self['xep_0380'].mechanisms[eme_ns]}
|
||||
# message['eme'] = {'namespace': eme_ns}
|
||||
# message.append(response_encrypted)
|
||||
for namespace, message in response_encrypted.items():
|
||||
message['eme']['namespace'] = namespace
|
||||
message['eme']['name'] = self['xep_0380'].mechanisms[namespace]
|
||||
message.send()
|
||||
|
||||
|
||||
def send_omemo_oob(self, jid: JID, url_encrypted, chat_type, aesgcm=False):
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
# if not aesgcm: url_encrypted = saxutils.escape(url_encrypted)
|
||||
message = self.make_message(mto=jid, mfrom=jid_from, mtype=chat_type)
|
||||
eme_ns = 'eu.siacs.conversations.axolotl'
|
||||
# message['eme']['namespace'] = eme_ns
|
||||
# message['eme']['name'] = self['xep_0380'].mechanisms[eme_ns]
|
||||
message['eme'] = {'namespace': eme_ns}
|
||||
# message['eme'] = {'name': self['xep_0380'].mechanisms[eme_ns]}
|
||||
message['oob']['url'] = url_encrypted
|
||||
message.append(url_encrypted)
|
||||
message.send()
|
||||
|
||||
|
||||
# FIXME Solve this function
|
||||
def send_omemo_reply(self, message, response_encrypted):
|
||||
eme_ns = 'eu.siacs.conversations.axolotl'
|
||||
# message['eme']['namespace'] = eme_ns
|
||||
# message['eme']['name'] = self['xep_0380'].mechanisms[eme_ns]
|
||||
message['eme'] = {'namespace': eme_ns}
|
||||
# message['eme'] = {'name': self['xep_0380'].mechanisms[eme_ns]}
|
||||
message.append(response_encrypted)
|
||||
message.reply(message['body']).send()
|
||||
|
||||
|
||||
# NOTE We might want to add more characters
|
||||
# def escape_to_xml(raw_string):
|
||||
# escape_map = {
|
||||
|
|
|
@ -16,7 +16,6 @@ FIXME
|
|||
1) Save name of groupchat instead of jid as name
|
||||
|
||||
"""
|
||||
from asyncio import TimeoutError
|
||||
from slixmpp.exceptions import IqError, IqTimeout, PresenceError
|
||||
from slixfeed.log import Logger
|
||||
|
||||
|
@ -47,7 +46,7 @@ class XmppMuc:
|
|||
# )
|
||||
logger.info('Joining groupchat\nJID : {}\n'.format(jid))
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
if not alias: alias = self.alias
|
||||
if alias == None: self.alias
|
||||
try:
|
||||
await self.plugin['xep_0045'].join_muc_wait(jid,
|
||||
alias,
|
||||
|
@ -69,11 +68,6 @@ class XmppMuc:
|
|||
logger.error(str(e))
|
||||
logger.error(jid)
|
||||
result = 'timeout'
|
||||
except TimeoutError as e:
|
||||
logger.error('Timeout AsyncIO')
|
||||
logger.error(str(e))
|
||||
logger.error(jid)
|
||||
result = 'timeout'
|
||||
except PresenceError as e:
|
||||
logger.error('Error Presence')
|
||||
logger.error(str(e))
|
||||
|
@ -81,9 +75,6 @@ class XmppMuc:
|
|||
e.presence['error']['code'] == '403'):
|
||||
logger.warning('{} is banned from {}'.format(self.alias, jid))
|
||||
result = 'ban'
|
||||
elif e.condition == 'conflict':
|
||||
logger.warning(e.presence['error']['text'])
|
||||
result = 'conflict'
|
||||
else:
|
||||
result = 'error'
|
||||
return result
|
||||
|
|
|
@ -27,6 +27,7 @@ TODO
|
|||
|
||||
import glob
|
||||
from slixfeed.config import Config
|
||||
import slixfeed.config as config
|
||||
from slixfeed.log import Logger
|
||||
from slixmpp.exceptions import IqTimeout, IqError
|
||||
import os
|
||||
|
@ -56,7 +57,9 @@ async def update(self):
|
|||
|
||||
|
||||
async def set_avatar(self):
|
||||
config_dir = self.dir_config
|
||||
config_dir = config.get_default_config_directory()
|
||||
if not os.path.isdir(config_dir):
|
||||
config_dir = '/usr/share/slixfeed/'
|
||||
filename = glob.glob(config_dir + '/image.*')
|
||||
if not filename and os.path.isdir('/usr/share/slixfeed/'):
|
||||
# filename = '/usr/share/slixfeed/image.svg'
|
||||
|
@ -108,7 +111,8 @@ def set_identity(self, category):
|
|||
|
||||
async def set_vcard(self):
|
||||
vcard = self.plugin['xep_0054'].make_vcard()
|
||||
profile = self.data_accounts_xmpp['profile']
|
||||
for key in profile: vcard[key] = profile[key]
|
||||
profile = config.get_values('accounts.toml', 'xmpp')['profile']
|
||||
for key in profile:
|
||||
vcard[key] = profile[key]
|
||||
await self.plugin['xep_0054'].publish_vcard(vcard)
|
||||
|
||||
|
|
|
@ -9,14 +9,14 @@ Functions create_node and create_entry are derived from project atomtopubsub.
|
|||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import os
|
||||
import slixmpp.plugins.xep_0060.stanza.pubsub as pubsub
|
||||
from slixmpp.xmlstream import ET
|
||||
import slixfeed.config as config
|
||||
from slixfeed.config import Config
|
||||
from slixfeed.log import Logger
|
||||
import slixfeed.sqlite as sqlite
|
||||
from slixfeed.syndication import Feed
|
||||
from slixfeed.utilities import Database, String, Url, Utilities
|
||||
from slixfeed.utilities import String, Url, Utilities
|
||||
from slixfeed.xmpp.iq import XmppIQ
|
||||
import sys
|
||||
|
||||
|
@ -44,10 +44,10 @@ class XmppPubsub:
|
|||
return results
|
||||
|
||||
|
||||
async def get_node_properties(self, jid_bare, node):
|
||||
config = await self.plugin['xep_0060'].get_node_config(jid_bare, node)
|
||||
subscriptions = await self.plugin['xep_0060'].get_node_subscriptions(jid_bare, node)
|
||||
affiliations = await self.plugin['xep_0060'].get_node_affiliations(jid_bare, node)
|
||||
async def get_node_properties(self, jid, node):
|
||||
config = await self.plugin['xep_0060'].get_node_config(jid, node)
|
||||
subscriptions = await self.plugin['xep_0060'].get_node_subscriptions(jid, node)
|
||||
affiliations = await self.plugin['xep_0060'].get_node_affiliations(jid, node)
|
||||
properties = {'config': config,
|
||||
'subscriptions': subscriptions,
|
||||
'affiliations': affiliations}
|
||||
|
@ -55,48 +55,49 @@ class XmppPubsub:
|
|||
return properties
|
||||
|
||||
|
||||
|
||||
async def get_node_configuration(self, jid_bare, node_id):
|
||||
node = await self.plugin['xep_0060'].get_node_config(jid_bare, node_id)
|
||||
async def get_node_configuration(self, jid, node_id):
|
||||
node = await self.plugin['xep_0060'].get_node_config(jid, node_id)
|
||||
if not node:
|
||||
print('NODE CONFIG', node_id, str(node))
|
||||
return node
|
||||
|
||||
|
||||
async def get_nodes(self, jid_bare):
|
||||
nodes = await self.plugin['xep_0060'].get_nodes(jid_bare)
|
||||
async def get_nodes(self, jid):
|
||||
nodes = await self.plugin['xep_0060'].get_nodes(jid)
|
||||
# 'self' would lead to slixmpp.jid.InvalidJID: idna validation failed:
|
||||
return nodes
|
||||
|
||||
|
||||
async def get_item(self, jid_bare, node, item_id):
|
||||
item = await self.plugin['xep_0060'].get_item(jid_bare, node, item_id)
|
||||
async def get_item(self, jid, node, item_id):
|
||||
item = await self.plugin['xep_0060'].get_item(jid, node, item_id)
|
||||
return item
|
||||
|
||||
|
||||
async def get_items(self, jid_bare, node):
|
||||
items = await self.plugin['xep_0060'].get_items(jid_bare, node)
|
||||
async def get_items(self, jid, node):
|
||||
items = await self.plugin['xep_0060'].get_items(jid, node)
|
||||
return items
|
||||
|
||||
|
||||
def delete_node(self, jid_bare, node):
|
||||
jid_from = self.boundjid.bare if self.is_component else None
|
||||
self.plugin['xep_0060'].delete_node(jid_bare, node, ifrom=jid_from)
|
||||
def delete_node(self, jid, node):
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
self.plugin['xep_0060'].delete_node(jid, node, ifrom=jid_from)
|
||||
|
||||
|
||||
def purge_node(self, jid_bare, node):
|
||||
jid_from = self.boundjid.bare if self.is_component else None
|
||||
self.plugin['xep_0060'].purge(jid_bare, node, ifrom=jid_from)
|
||||
def purge_node(self, jid, node):
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
self.plugin['xep_0060'].purge(jid, node, ifrom=jid_from)
|
||||
# iq = self.Iq(stype='set',
|
||||
# sto=jid_bare,
|
||||
# sto=jid,
|
||||
# sfrom=jid_from)
|
||||
# iq['pubsub']['purge']['node'] = node
|
||||
# return iq
|
||||
|
||||
|
||||
# TODO Make use of var "xep" with match/case (XEP-0060, XEP-0277, XEP-0472)
|
||||
def create_node(self, jid_bare, node, xep=None ,title=None, subtitle=None):
|
||||
jid_from = self.boundjid.bare if self.is_component else None
|
||||
def create_node(self, jid, node, xep ,title=None, subtitle=None):
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
iq = self.Iq(stype='set',
|
||||
sto=jid_bare,
|
||||
sto=jid,
|
||||
sfrom=jid_from)
|
||||
iq['pubsub']['create']['node'] = node
|
||||
form = iq['pubsub']['configure']['form']
|
||||
|
@ -130,8 +131,8 @@ class XmppPubsub:
|
|||
|
||||
# TODO Consider to create a separate function called "create_atom_entry"
|
||||
# or "create_rfc4287_entry" for anything related to variable "node_entry".
|
||||
def create_entry(self, jid_bare, node_id, item_id, node_item):
|
||||
iq = self.Iq(stype="set", sto=jid_bare)
|
||||
def create_entry(self, jid, node_id, item_id, node_item):
|
||||
iq = self.Iq(stype="set", sto=jid)
|
||||
iq['pubsub']['publish']['node'] = node_id
|
||||
|
||||
item = pubsub.Item()
|
||||
|
@ -152,8 +153,8 @@ class XmppPubsub:
|
|||
return iq
|
||||
|
||||
|
||||
def _create_entry(self, jid_bare, node, entry, version):
|
||||
iq = self.Iq(stype="set", sto=jid_bare)
|
||||
def _create_entry(self, jid, node, entry, version):
|
||||
iq = self.Iq(stype="set", sto=jid)
|
||||
iq['pubsub']['publish']['node'] = node
|
||||
|
||||
item = pubsub.Item()
|
||||
|
@ -259,10 +260,21 @@ class XmppPubsubAction:
|
|||
async def send_selected_entry(self, jid_bare, node_id, entry_id):
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: jid_bare: {}'.format(function_name, jid_bare))
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
feed_id = sqlite.get_feed_id_by_entry_index(db_file, entry_id)
|
||||
feed_id = feed_id[0]
|
||||
node_id, node_title, node_subtitle = sqlite.get_feed_properties(db_file, feed_id)
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
report = {}
|
||||
if jid_bare == self.boundjid.bare:
|
||||
node_id = 'urn:xmpp:microblog:0'
|
||||
node_subtitle = None
|
||||
node_title = None
|
||||
else:
|
||||
feed_id = sqlite.get_feed_id_by_entry_index(db_file, entry_id)
|
||||
feed_id = feed_id[0]
|
||||
node_id, node_title, node_subtitle = sqlite.get_feed_properties(db_file, feed_id)
|
||||
print('THIS IS A TEST')
|
||||
print(node_id)
|
||||
print(node_title)
|
||||
print(node_subtitle)
|
||||
print('THIS IS A TEST')
|
||||
xep = None
|
||||
iq_create_node = XmppPubsub.create_node(
|
||||
self, jid_bare, node_id, xep, node_title, node_subtitle)
|
||||
|
@ -272,14 +284,14 @@ class XmppPubsubAction:
|
|||
print(node_id)
|
||||
entry_dict = Feed.pack_entry_into_dict(db_file, entry)
|
||||
node_item = Feed.create_rfc4287_entry(entry_dict)
|
||||
item_id = Utilities.hash_url_to_md5(entry_dict['link'])
|
||||
entry_url = entry_dict['link']
|
||||
item_id = Utilities.hash_url_to_md5(entry_url)
|
||||
iq_create_entry = XmppPubsub.create_entry(
|
||||
self, jid_bare, node_id, item_id, node_item)
|
||||
await XmppIQ.send(self, iq_create_entry)
|
||||
await sqlite.mark_as_read(db_file, entry_id)
|
||||
|
||||
# NOTE This value is returned for the sake of testing
|
||||
return entry_dict['link']
|
||||
report = entry_url
|
||||
return report
|
||||
|
||||
|
||||
async def send_unread_items(self, jid_bare):
|
||||
|
@ -287,7 +299,7 @@ class XmppPubsubAction:
|
|||
|
||||
Parameters
|
||||
----------
|
||||
jid_bare : str
|
||||
jid_bare : TYPE
|
||||
Bare Jabber ID.
|
||||
|
||||
Returns
|
||||
|
@ -298,7 +310,7 @@ class XmppPubsubAction:
|
|||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: jid_bare: {}'.format(function_name, jid_bare))
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
report = {}
|
||||
subscriptions = sqlite.get_active_feeds_url(db_file)
|
||||
for url in subscriptions:
|
||||
|
@ -308,48 +320,41 @@ class XmppPubsubAction:
|
|||
# feed_properties = sqlite.get_feed_properties(db_file, feed_id)
|
||||
feed_id = sqlite.get_feed_id(db_file, url)
|
||||
feed_id = feed_id[0]
|
||||
|
||||
|
||||
# Publish to node 'urn:xmpp:microblog:0' for own JID
|
||||
# Publish to node based on feed identifier for PubSub service.
|
||||
|
||||
# node_id = feed_properties[2]
|
||||
# node_title = feed_properties[3]
|
||||
# node_subtitle = feed_properties[5]
|
||||
node_id = sqlite.get_feed_identifier(db_file, feed_id)
|
||||
node_id = node_id[0]
|
||||
if not node_id:
|
||||
counter = 0
|
||||
while True:
|
||||
identifier = String.generate_identifier(url, counter)
|
||||
if sqlite.check_identifier_exist(db_file, identifier):
|
||||
counter += 1
|
||||
else:
|
||||
break
|
||||
await sqlite.update_feed_identifier(db_file, feed_id, identifier)
|
||||
|
||||
if jid_bare == self.boundjid.bare:
|
||||
node_id = 'urn:xmpp:microblog:0'
|
||||
node_subtitle = None
|
||||
node_title = None
|
||||
else:
|
||||
# node_id = feed_properties[2]
|
||||
# node_title = feed_properties[3]
|
||||
# node_subtitle = feed_properties[5]
|
||||
node_id = sqlite.get_feed_identifier(db_file, feed_id)
|
||||
node_id = node_id[0]
|
||||
node_title = sqlite.get_feed_title(db_file, feed_id)
|
||||
node_title = node_title[0]
|
||||
node_subtitle = sqlite.get_feed_subtitle(db_file, feed_id)
|
||||
node_subtitle = node_subtitle[0]
|
||||
if not node_id:
|
||||
counter = 0
|
||||
while True:
|
||||
identifier = String.generate_identifier(url, counter)
|
||||
if sqlite.check_identifier_exist(db_file, identifier):
|
||||
counter += 1
|
||||
else:
|
||||
break
|
||||
await sqlite.update_feed_identifier(db_file, feed_id, identifier)
|
||||
node_id = sqlite.get_feed_identifier(db_file, feed_id)
|
||||
node_id = node_id[0]
|
||||
node_title = sqlite.get_feed_title(db_file, feed_id)
|
||||
node_title = node_title[0]
|
||||
node_subtitle = sqlite.get_feed_subtitle(db_file, feed_id)
|
||||
node_subtitle = node_subtitle[0]
|
||||
xep = None
|
||||
#node_exist = await XmppPubsub.get_node_configuration(self, jid_bare, node_id)
|
||||
nodes = await XmppPubsub.get_nodes(self, jid_bare)
|
||||
node_items = nodes['disco_items']['items']
|
||||
node_exist = False
|
||||
for node_item in node_items:
|
||||
if node_item[1] == node_id:
|
||||
node_exist = True
|
||||
break
|
||||
print(['node_exist', node_exist])
|
||||
node_exist = await XmppPubsub.get_node_configuration(self, jid_bare, node_id)
|
||||
if not node_exist:
|
||||
iq_create_node = XmppPubsub.create_node(
|
||||
self, jid_bare, node_id, xep, node_title, node_subtitle)
|
||||
result = await XmppIQ.send(self, iq_create_node)
|
||||
result_condition = result.iq['error']['condition']
|
||||
if result_condition in ('forbidden', 'service-unavailable'):
|
||||
reason = result.iq['error']['text']
|
||||
print('Creation of node {} for JID {} has failed'.format(node_id, jid_bare, reason))
|
||||
return
|
||||
await XmppIQ.send(self, iq_create_node)
|
||||
entries = sqlite.get_unread_entries_of_feed(db_file, feed_id)
|
||||
report[url] = len(entries)
|
||||
for entry in entries:
|
||||
|
@ -357,63 +362,24 @@ class XmppPubsubAction:
|
|||
node_entry = Feed.create_rfc4287_entry(feed_entry)
|
||||
entry_url = feed_entry['link']
|
||||
item_id = Utilities.hash_url_to_md5(entry_url)
|
||||
print(['PubSub node item was sent to', jid_bare, node_id])
|
||||
print([entry_url, item_id])
|
||||
print('PubSub node item was sent to', jid_bare, node_id)
|
||||
print(entry_url)
|
||||
print(item_id)
|
||||
iq_create_entry = XmppPubsub.create_entry(
|
||||
self, jid_bare, node_id, item_id, node_entry)
|
||||
result = await XmppIQ.send(self, iq_create_entry)
|
||||
await XmppIQ.send(self, iq_create_entry)
|
||||
ix = entry[0]
|
||||
await sqlite.mark_as_read(db_file, ix)
|
||||
print(report)
|
||||
return report
|
||||
|
||||
|
||||
class XmppPubsubTask:
|
||||
|
||||
|
||||
async def loop_task(self, jid_bare):
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
if jid_bare not in self.settings:
|
||||
Config.add_settings_jid(self, jid_bare, db_file)
|
||||
while True:
|
||||
print('Looping task "publish" for JID {}'.format(jid_bare))
|
||||
if jid_bare not in self.task_manager:
|
||||
self.task_manager[jid_bare] = {}
|
||||
logger.info('Creating new task manager for JID {}'.format(jid_bare))
|
||||
logger.info('Stopping task "publish" for JID {}'.format(jid_bare))
|
||||
try:
|
||||
self.task_manager[jid_bare]['publish'].cancel()
|
||||
except:
|
||||
logger.info('No task "publish" for JID {} (XmppPubsubAction.send_unread_items)'
|
||||
.format(jid_bare))
|
||||
logger.info('Starting tasks "publish" for JID {}'.format(jid_bare))
|
||||
self.task_manager[jid_bare]['publish'] = asyncio.create_task(
|
||||
XmppPubsubAction.send_unread_items(self, jid_bare))
|
||||
await asyncio.sleep(60 * 180)
|
||||
|
||||
|
||||
def restart_task(self, jid_bare):
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
if jid_bare not in self.settings:
|
||||
Config.add_settings_jid(self, jid_bare, db_file)
|
||||
if jid_bare not in self.task_manager:
|
||||
self.task_manager[jid_bare] = {}
|
||||
logger.info('Creating new task manager for JID {}'.format(jid_bare))
|
||||
logger.info('Stopping task "publish" for JID {}'.format(jid_bare))
|
||||
try:
|
||||
self.task_manager[jid_bare]['publish'].cancel()
|
||||
except:
|
||||
logger.info('No task "publish" for JID {} (XmppPubsubAction.send_unread_items)'
|
||||
.format(jid_bare))
|
||||
logger.info('Starting tasks "publish" for JID {}'.format(jid_bare))
|
||||
self.task_manager[jid_bare]['publish'] = asyncio.create_task(
|
||||
XmppPubsubAction.send_unread_items(self, jid_bare))
|
||||
|
||||
|
||||
async def task_publish(self, jid_bare):
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
if jid_bare not in self.settings:
|
||||
Config.add_settings_jid(self, jid_bare, db_file)
|
||||
Config.add_settings_jid(self.settings, jid_bare, db_file)
|
||||
while True:
|
||||
await XmppPubsubAction.send_unread_items(self, jid_bare)
|
||||
await asyncio.sleep(60 * 180)
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from slixfeed.config import Config
|
||||
import slixfeed.config as config
|
||||
import slixfeed.sqlite as sqlite
|
||||
from slixfeed.log import Logger
|
||||
from slixfeed.xmpp.presence import XmppPresence
|
||||
|
@ -25,11 +25,11 @@ class XmppStatus:
|
|||
Jabber ID.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug(f'{function_name}: jid: {jid_bare}')
|
||||
logger.debug('{}: jid: {}'.format(function_name, jid_bare))
|
||||
status_text = '📜️ Slixfeed RSS News Bot'
|
||||
enabled = Config.get_setting_value(self, jid_bare, 'enabled')
|
||||
db_file = config.get_pathname_to_database(jid_bare)
|
||||
enabled = Config.get_setting_value(self.settings, jid_bare, 'enabled')
|
||||
if enabled:
|
||||
db_file = Database.instantiate(self.dir_data, jid_bare)
|
||||
jid_task = self.pending_tasks[jid_bare] if jid_bare in self.pending_tasks else None
|
||||
if jid_task and len(jid_task):
|
||||
# print('status dnd for ' + jid_bare)
|
||||
|
@ -47,10 +47,10 @@ class XmppStatus:
|
|||
if unread:
|
||||
# print('status unread for ' + jid_bare)
|
||||
status_mode = 'chat'
|
||||
status_text = f'📬️ There are {str(unread)} news items'
|
||||
status_text = '📬️ There are {} news items'.format(str(unread))
|
||||
else:
|
||||
# print('status no news for ' + jid_bare)
|
||||
status_mode = 'away'
|
||||
status_mode = 'available'
|
||||
status_text = '📭️ No news'
|
||||
else:
|
||||
# print('status disabled for ' + jid_bare)
|
||||
|
@ -73,13 +73,14 @@ class XmppStatusTask:
|
|||
return
|
||||
if jid_bare not in self.task_manager:
|
||||
self.task_manager[jid_bare] = {}
|
||||
logger.info('Creating new task manager for JID {jid_bare}')
|
||||
logger.info('Stopping task "status" for JID {jid_bare}')
|
||||
logger.info('Creating new task manager for JID {}'.format(jid_bare))
|
||||
logger.info('Stopping task "status" for JID {}'.format(jid_bare))
|
||||
try:
|
||||
self.task_manager[jid_bare]['status'].cancel()
|
||||
except:
|
||||
logger.info(f'No task "status" for JID {jid_bare} (XmppStatusTask.start_task)')
|
||||
logger.info(f'Starting tasks "status" for JID {jid_bare}')
|
||||
logger.info('No task "status" for JID {} (XmppStatusTask.start_task)'
|
||||
.format(jid_bare))
|
||||
logger.info('Starting tasks "status" for JID {}'.format(jid_bare))
|
||||
self.task_manager[jid_bare]['status'] = asyncio.create_task(
|
||||
XmppStatusTask.task_status(self, jid_bare))
|
||||
|
||||
|
@ -89,4 +90,5 @@ class XmppStatusTask:
|
|||
'status' in self.task_manager[jid_bare]):
|
||||
self.task_manager[jid_bare]['status'].cancel()
|
||||
else:
|
||||
logger.debug(f'No task "status" for JID {jid_bare}')
|
||||
logger.debug('No task "status" for JID {}'
|
||||
.format(jid_bare))
|
|
@ -6,51 +6,47 @@ Based on http_upload.py example from project slixmpp
|
|||
https://codeberg.org/poezio/slixmpp/src/branch/master/examples/http_upload.py
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from slixfeed.log import Logger
|
||||
from slixmpp import JID
|
||||
from slixmpp.exceptions import IqTimeout, IqError
|
||||
from slixmpp.plugins.xep_0363.http_upload import HTTPError
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
logger = Logger(__name__)
|
||||
# import sys
|
||||
|
||||
class XmppUpload:
|
||||
|
||||
async def start(self, jid, filename: Path, size: Optional[int] = None,
|
||||
encrypted: bool = False, domain: Optional[JID] = None):
|
||||
async def start(self, jid, filename, domain=None):
|
||||
logger.info(['Uploading file %s...', filename])
|
||||
try:
|
||||
upload_file = self['xep_0363'].upload_file
|
||||
if encrypted and not self['xep_0454']:
|
||||
print(
|
||||
'The xep_0454 module isn\'t available. '
|
||||
'Ensure you have \'cryptography\' '
|
||||
'from extras_require installed.',
|
||||
file=sys.stderr,
|
||||
)
|
||||
url = None
|
||||
elif encrypted:
|
||||
upload_file = self['xep_0454'].upload_file
|
||||
# if self.encrypted and not self['xep_0454']:
|
||||
# print(
|
||||
# 'The xep_0454 module isn\'t available. '
|
||||
# 'Ensure you have \'cryptography\' '
|
||||
# 'from extras_require installed.',
|
||||
# file=sys.stderr,
|
||||
# )
|
||||
# return
|
||||
# elif self.encrypted:
|
||||
# upload_file = self['xep_0454'].upload_file
|
||||
try:
|
||||
url = await upload_file(filename, size, domain, timeout=10,)
|
||||
url = await upload_file(
|
||||
filename, domain, timeout=10,
|
||||
)
|
||||
logger.info('Upload successful!')
|
||||
logger.info(['Sending file to %s', jid])
|
||||
except HTTPError:
|
||||
url = None
|
||||
url = ('Error: It appears that this server does not support '
|
||||
'HTTP File Upload.')
|
||||
logger.error('It appears that this server does not support '
|
||||
'HTTP File Upload.')
|
||||
# raise HTTPError(
|
||||
# "This server doesn't appear to support HTTP File Upload"
|
||||
# )
|
||||
except IqError as e:
|
||||
url = None
|
||||
logger.error('Could not send message')
|
||||
logger.error(e)
|
||||
except IqTimeout as e:
|
||||
url = None
|
||||
# raise TimeoutError('Could not send message in time')
|
||||
logger.error('Could not send message in time')
|
||||
logger.error(e)
|
||||
|
|
|
@ -9,14 +9,10 @@ logger = Logger(__name__)
|
|||
# class XmppChat
|
||||
# class XmppUtility:
|
||||
|
||||
|
||||
class XmppUtilities:
|
||||
|
||||
def get_self_alias(self, room):
|
||||
"""Get self alias of a given group chat"""
|
||||
jid_full = self.plugin['xep_0045'].get_our_jid_in_room(room)
|
||||
alias = jid_full.split('/')[1]
|
||||
return alias
|
||||
|
||||
|
||||
async def get_chat_type(self, jid):
|
||||
"""
|
||||
Check chat (i.e. JID) type.
|
||||
|
@ -64,18 +60,21 @@ class XmppUtilities:
|
|||
return result
|
||||
|
||||
|
||||
def is_access(self, jid, chat_type):
|
||||
"""Determine access privilege"""
|
||||
room = jid_bare = jid.bare
|
||||
alias = jid.resource
|
||||
if chat_type == 'groupchat':
|
||||
access = True if XmppUtilities.is_moderator(self, room, alias) else False
|
||||
if access: print('Access granted to groupchat moderator ' + alias)
|
||||
else:
|
||||
print('Access granted to chat jid ' + jid_bare)
|
||||
access = True
|
||||
return access
|
||||
|
||||
def is_access(self, jid_bare, jid_full, chat_type):
|
||||
"""Determine access privilege"""
|
||||
operator = XmppUtilities.is_operator(self, jid_bare)
|
||||
if operator:
|
||||
if chat_type == 'groupchat':
|
||||
if XmppUtilities.is_moderator(self, jid_bare, jid_full):
|
||||
access = True
|
||||
else:
|
||||
access = True
|
||||
else:
|
||||
access = False
|
||||
return access
|
||||
|
||||
|
||||
def is_operator(self, jid_bare):
|
||||
"""Check if given JID is an operator"""
|
||||
result = False
|
||||
|
@ -85,29 +84,25 @@ class XmppUtilities:
|
|||
# operator_name = operator['name']
|
||||
break
|
||||
return result
|
||||
|
||||
def is_admin(self, room, alias):
|
||||
"""Check if given JID is an administrator"""
|
||||
affiliation = self.plugin['xep_0045'].get_jid_property(room, alias, 'affiliation')
|
||||
result = True if affiliation == 'admin' else False
|
||||
return result
|
||||
|
||||
def is_owner(self, room, alias):
|
||||
"""Check if given JID is an owner"""
|
||||
affiliation = self.plugin['xep_0045'].get_jid_property(room, alias, 'affiliation')
|
||||
result = True if affiliation == 'owner' else False
|
||||
return result
|
||||
|
||||
def is_moderator(self, room, alias):
|
||||
|
||||
|
||||
def is_moderator(self, jid_bare, jid_full):
|
||||
"""Check if given JID is a moderator"""
|
||||
role = self.plugin['xep_0045'].get_jid_property(room, alias, 'role')
|
||||
result = True if role == 'moderator' else False
|
||||
alias = jid_full[jid_full.index('/')+1:]
|
||||
role = self.plugin['xep_0045'].get_jid_property(jid_bare, alias, 'role')
|
||||
if role == 'moderator':
|
||||
result = True
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
|
||||
# NOTE Would this properly work when Alias and Local differ?
|
||||
|
||||
|
||||
def is_member(self, jid_bare, jid_full):
|
||||
"""Check if given JID is a member"""
|
||||
alias = jid_full[jid_full.index('/')+1:]
|
||||
affiliation = self.plugin['xep_0045'].get_jid_property(jid_bare, alias, 'affiliation')
|
||||
result = True if affiliation == 'member' else False
|
||||
return result
|
||||
if affiliation == 'member':
|
||||
result = True
|
||||
else:
|
||||
result = False
|
||||
return result
|
Loading…
Reference in a new issue