forked from sch/Slixfeed
Compare commits
22 commits
Author | SHA1 | Date | |
---|---|---|---|
|
263382ba8d | ||
|
640677437c | ||
|
2a9d2657b1 | ||
|
e63a5e4a74 | ||
|
735ce58178 | ||
|
70a62d3e33 | ||
|
95dd1f82e4 | ||
|
f3f58244e8 | ||
|
26185f96d7 | ||
|
c050c765dd | ||
|
178f49cb86 | ||
|
3913f740ef | ||
|
ed7491b5a9 | ||
|
4642aa694b | ||
|
21cb7df160 | ||
|
bd058ebf73 | ||
|
9c068ad600 | ||
|
6714089bdd | ||
|
51943b5b0c | ||
|
f9a1c683cd | ||
|
5e20b2830d | ||
|
37444c9d4e |
32 changed files with 1876 additions and 526 deletions
22
README.md
22
README.md
|
@ -20,14 +20,15 @@ Slixfeed is primarily designed for XMPP (aka Jabber), yet it is built to be exte
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Visual interface** - Interactive interface for XMPP using Ad-Hoc Commands,
|
|
||||||
- **Ease** - Slixfeed automatically scans (i.e. crawls) for syndication feeds of given URL.
|
- **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.
|
- **Export** - Download articles as ePUB, HTML, Markdown and PDF.
|
||||||
- **Filtering** - Filter news items using lists of allow and deny.
|
- **Filtering** - Filter news items using lists of allow and deny.
|
||||||
- **Multimedia** - Display audios pictures and videos inline.
|
- **Multimedia** - Display audios pictures and videos inline.
|
||||||
- **Privacy** - Redirect to alternative back-ends, such as Invidious, Librarian, Nitter, for increased privacy, productivity and security.
|
- **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.
|
- **Portable** - Export and import feeds with a standard OPML file.
|
||||||
- **Simultaneous** - Slixfeed is designed to handle multiple contacts, including groupchats, Simultaneously.
|
- **Simultaneous** - Slixfeed is designed to handle multiple contacts, including groupchats, Simultaneously.
|
||||||
|
- **Visual interface** - Interactive interface for XMPP using Ad-Hoc Commands,
|
||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
|
|
||||||
|
@ -56,7 +57,18 @@ It is possible to install Slixfeed using pip and pipx.
|
||||||
```
|
```
|
||||||
$ python3 -m venv .venv
|
$ python3 -m venv .venv
|
||||||
$ source .venv/bin/activate
|
$ source .venv/bin/activate
|
||||||
$ pip install git+https://gitgud.io/sjehuda/slixfeed
|
```
|
||||||
|
|
||||||
|
##### Install
|
||||||
|
|
||||||
|
```
|
||||||
|
$ pip install git+https://git.xmpp-it.net/sch/Slixfeed
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Install (OMEMO)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ pip install git+https://git.xmpp-it.net/sch/Slixfeed[omemo]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### pipx
|
#### pipx
|
||||||
|
@ -64,14 +76,14 @@ $ pip install git+https://gitgud.io/sjehuda/slixfeed
|
||||||
##### Install
|
##### Install
|
||||||
|
|
||||||
```
|
```
|
||||||
$ pipx install git+https://gitgud.io/sjehuda/slixfeed
|
$ pipx install git+https://git.xmpp-it.net/sch/Slixfeed
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Update
|
##### Update
|
||||||
|
|
||||||
```
|
```
|
||||||
$ pipx uninstall slixfeed
|
$ pipx uninstall slixfeed
|
||||||
$ pipx install git+https://gitgud.io/sjehuda/slixfeed
|
$ pipx install git+https://git.xmpp-it.net/sch/Slixfeed
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start
|
### Start
|
||||||
|
@ -87,7 +99,7 @@ $ slixfeed
|
||||||
|
|
||||||
## Recommended Clients
|
## 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), [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), [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).
|
||||||
|
|
||||||
### Support
|
### Support
|
||||||
|
|
||||||
|
|
|
@ -37,27 +37,28 @@ keywords = [
|
||||||
"xml",
|
"xml",
|
||||||
"xmpp",
|
"xmpp",
|
||||||
]
|
]
|
||||||
|
|
||||||
# urls = {Homepage = "https://gitgud.io/sjehuda/slixfeed"}
|
# urls = {Homepage = "https://gitgud.io/sjehuda/slixfeed"}
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aiofiles",
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
# "daemonize",
|
# "daemonize",
|
||||||
"feedparser",
|
"feedparser",
|
||||||
"lxml",
|
"lxml",
|
||||||
# "pysocks",
|
|
||||||
"python-dateutil",
|
"python-dateutil",
|
||||||
"requests",
|
|
||||||
"slixmpp",
|
"slixmpp",
|
||||||
"tomli", # Python 3.10
|
"tomli", # Python 3.10
|
||||||
"tomli_w",
|
"tomli_w",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "http://slixfeed.i2p/"
|
Homepage = "https://slixfeed.woodpeckersnest.space"
|
||||||
Repository = "https://gitgud.io/sjehuda/slixfeed"
|
Repository = "https://git.xmpp-it.net/sch/Slixfeed"
|
||||||
Issues = "https://gitgud.io/sjehuda/slixfeed/issues"
|
Issues = "https://gitgud.io/sjehuda/slixfeed/issues"
|
||||||
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
omemo = ["slixmpp-omemo"]
|
||||||
proxy = ["pysocks"]
|
proxy = ["pysocks"]
|
||||||
|
|
||||||
# [project.readme]
|
# [project.readme]
|
||||||
|
|
|
@ -123,10 +123,6 @@ def main():
|
||||||
# Setup logging.
|
# Setup logging.
|
||||||
logging.basicConfig(level=args.loglevel,
|
logging.basicConfig(level=args.loglevel,
|
||||||
format='%(levelname)-8s %(message)s')
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
# # Setup logging.
|
|
||||||
# logging.basicConfig(level=args.loglevel,
|
|
||||||
# format='%(levelname)-8s %(message)s')
|
|
||||||
# # logging.basicConfig(format='[%(levelname)s] %(message)s')
|
# # logging.basicConfig(format='[%(levelname)s] %(message)s')
|
||||||
# logger = logging.getLogger()
|
# logger = logging.getLogger()
|
||||||
# logdbg = logger.debug
|
# logdbg = logger.debug
|
||||||
|
|
|
@ -28,9 +28,9 @@ Good luck!
|
||||||
|
|
||||||
filetypes = "Atom, JSON, RDF, RSS, XML."
|
filetypes = "Atom, JSON, RDF, RSS, XML."
|
||||||
platforms = "XMPP"
|
platforms = "XMPP"
|
||||||
# platforms = "ActivityPub, Briar, DeltaChat, Email, IRC, LXMF, MQTT, Nostr, Session, Tox."
|
# platforms = "ActivityPub, BitMessage, Briar, DeltaChat, Email, IRC, LXMF, MQTT, Nostr, Session, Tox."
|
||||||
comment = "For ideal experience, we recommend using XMPP." # Nostr, Session or DeltaChat
|
comment = "For ideal experience, we recommend using XMPP." # Nostr, Session or DeltaChat
|
||||||
url = "https://gitgud.io/sjehuda/slixfeed"
|
url = "https://git.xmpp-it.net/sch/Slixfeed"
|
||||||
|
|
||||||
[[about]]
|
[[about]]
|
||||||
name = "slixmpp"
|
name = "slixmpp"
|
||||||
|
@ -245,7 +245,7 @@ and webhooks.
|
||||||
User ⇄ XMPP client ⇄ XMPP Server ⇄ XMPP Bot ⇄ REST API
|
User ⇄ XMPP client ⇄ XMPP Server ⇄ XMPP Bot ⇄ REST API
|
||||||
"""]
|
"""]
|
||||||
interface = "Groupchat"
|
interface = "Groupchat"
|
||||||
url = "https://github.com/nioc/xmpp-bot"
|
url = "https://git.xmpp-it.net/roughnecks/xmpp-bot"
|
||||||
|
|
||||||
[[legal]]
|
[[legal]]
|
||||||
title = "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 \
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR \
|
||||||
A PARTICULAR PURPOSE. See the MIT License for more details.
|
A PARTICULAR PURPOSE. See the MIT License for more details.
|
||||||
"""]
|
"""]
|
||||||
link = "https://gitgud.io/sjehuda/slixfeed"
|
link = "https://git.xmpp-it.net/sch/Slixfeed"
|
||||||
|
|
||||||
[[license]]
|
[[license]]
|
||||||
title = "License"
|
title = "License"
|
||||||
|
@ -653,10 +653,15 @@ or on your desktop.
|
||||||
url = "https://conversejs.org"
|
url = "https://conversejs.org"
|
||||||
platform = "HTML (Web)"
|
platform = "HTML (Web)"
|
||||||
|
|
||||||
# [[clients]]
|
[[clients]]
|
||||||
# name = "Gajim"
|
name = "Gajim"
|
||||||
# info = "XMPP client for desktop"
|
info = "XMPP client for desktop"
|
||||||
# url = "https://gajim.org"
|
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]]
|
# [[clients]]
|
||||||
# name = "Monal IM"
|
# name = "Monal IM"
|
||||||
|
|
|
@ -213,6 +213,18 @@ read <url> <index>
|
||||||
Display specified entry number from given <url> by given <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]
|
[search]
|
||||||
feeds = """
|
feeds = """
|
||||||
feeds
|
feeds
|
||||||
|
|
|
@ -1 +1,48 @@
|
||||||
<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>
|
<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>
|
||||||
|
|
Before Width: | Height: | Size: 448 B After Width: | Height: | Size: 2 KiB |
48
slixfeed/assets/image_black.svg
Normal file
48
slixfeed/assets/image_black.svg
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 2 KiB |
51
slixfeed/assets/image_semi_transparent.svg
Normal file
51
slixfeed/assets/image_semi_transparent.svg
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 2.2 KiB |
48
slixfeed/assets/image_transparent.svg
Normal file
48
slixfeed/assets/image_transparent.svg
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 2 KiB |
48
slixfeed/assets/image_white.svg
Normal file
48
slixfeed/assets/image_white.svg
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 2 KiB |
|
@ -3,7 +3,7 @@ info = """
|
||||||
Slixfeed is a news broker bot for syndicated news which aims to be \
|
Slixfeed is a news broker bot for syndicated news which aims to be \
|
||||||
an easy to use and fully-featured news aggregating bot.
|
an easy to use and fully-featured news aggregating bot.
|
||||||
|
|
||||||
Slixfeed provides a convenient access to Blogs, News sites and \
|
Slixfeed provides a convenient access to Blogs, News websites and \
|
||||||
even Fediverse instances, along with filtering and other privacy \
|
even Fediverse instances, along with filtering and other privacy \
|
||||||
driven functionalities.
|
driven functionalities.
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ interval = 300 # Update interval (Minimum value 10)
|
||||||
length = 300 # Maximum length of summary (Value 0 to disable)
|
length = 300 # Maximum length of summary (Value 0 to disable)
|
||||||
media = 0 # Display media (audio, image, video) when available
|
media = 0 # Display media (audio, image, video) when available
|
||||||
old = 0 # Mark entries of newly added entries as unread
|
old = 0 # Mark entries of newly added entries as unread
|
||||||
|
omemo = 1 # Encrypt messages with OMEMO
|
||||||
quantum = 3 # Amount of entries per update
|
quantum = 3 # Amount of entries per update
|
||||||
random = 0 # Pick random item from database
|
random = 0 # Pick random item from database
|
||||||
|
|
||||||
|
|
|
@ -51,27 +51,31 @@ logger = Logger(__name__)
|
||||||
# setting_jid.setting_key has value, otherwise resort to setting_default.setting_key.
|
# setting_jid.setting_key has value, otherwise resort to setting_default.setting_key.
|
||||||
class Config:
|
class Config:
|
||||||
|
|
||||||
def add_settings_default(settings):
|
def add_settings_default(self):
|
||||||
settings_default = get_values('settings.toml', 'settings')
|
settings_default = get_values('settings.toml', 'settings')
|
||||||
settings['default'] = settings_default
|
self.defaults = settings_default
|
||||||
|
|
||||||
# TODO Open SQLite file once
|
# TODO Open SQLite file once
|
||||||
def add_settings_jid(settings, jid_bare, db_file):
|
def add_settings_jid(self, jid_bare, db_file):
|
||||||
settings[jid_bare] = {}
|
self.settings[jid_bare] = {}
|
||||||
for key in ('archive', 'enabled', 'filter', 'formatting', 'interval',
|
for key in self.defaults['default']:
|
||||||
'length', 'media', 'old', 'quantum'):
|
|
||||||
value = sqlite.get_setting_value(db_file, key)
|
value = sqlite.get_setting_value(db_file, key)
|
||||||
if value: settings[jid_bare][key] = value[0]
|
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]
|
||||||
|
|
||||||
def get_settings_xmpp(key=None):
|
def get_settings_xmpp(key=None):
|
||||||
result = get_values('accounts.toml', 'xmpp')
|
result = get_values('accounts.toml', 'xmpp')
|
||||||
result = result[key] if key else result
|
result = result[key] if key else result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def set_setting_value(settings, jid_bare, db_file, key, val):
|
async def set_setting_value(self, jid_bare, db_file, key, val):
|
||||||
key = key.lower()
|
key = key.lower()
|
||||||
key_val = [key, val]
|
key_val = [key, val]
|
||||||
settings[jid_bare][key] = val
|
self.settings[jid_bare][key] = val
|
||||||
if sqlite.is_setting_key(db_file, key):
|
if sqlite.is_setting_key(db_file, key):
|
||||||
await sqlite.update_setting_value(db_file, key_val)
|
await sqlite.update_setting_value(db_file, key_val)
|
||||||
else:
|
else:
|
||||||
|
@ -79,11 +83,11 @@ class Config:
|
||||||
|
|
||||||
# TODO Segregate Jabber ID settings from Slixfeed wide settings.
|
# TODO Segregate Jabber ID settings from Slixfeed wide settings.
|
||||||
# self.settings, self.settings_xmpp, self.settings_irc etc.
|
# self.settings, self.settings_xmpp, self.settings_irc etc.
|
||||||
def get_setting_value(settings, jid_bare, key):
|
def get_setting_value(self, jid_bare, key):
|
||||||
if jid_bare in settings and key in settings[jid_bare]:
|
if jid_bare in self.settings and key in self.settings[jid_bare]:
|
||||||
value = settings[jid_bare][key]
|
value = self.settings[jid_bare][key]
|
||||||
else:
|
else:
|
||||||
value = settings['default'][key]
|
value = self.defaults['default'][key]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
class ConfigNetwork:
|
class ConfigNetwork:
|
||||||
|
@ -105,6 +109,71 @@ class ConfigJabberID:
|
||||||
settings[jid_bare][key] = value
|
settings[jid_bare][key] = value
|
||||||
|
|
||||||
|
|
||||||
|
class Data:
|
||||||
|
|
||||||
|
|
||||||
|
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_pathname_to_omemo_directory():
|
||||||
|
"""
|
||||||
|
Get OMEMO directory.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
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 + "/omemo"):
|
||||||
|
os.mkdir(db_dir + "/omemo")
|
||||||
|
omemo_dir = os.path.join(db_dir, "omemo")
|
||||||
|
return omemo_dir
|
||||||
|
|
||||||
|
|
||||||
def get_values(filename, key=None):
|
def get_values(filename, key=None):
|
||||||
config_dir = get_default_config_directory()
|
config_dir = get_default_config_directory()
|
||||||
if not os.path.isdir(config_dir):
|
if not os.path.isdir(config_dir):
|
||||||
|
|
56
slixfeed/documentation/adhoc_commands_muc.md
Normal file
56
slixfeed/documentation/adhoc_commands_muc.md
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# 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,15 +36,18 @@ NOTE
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
from aiohttp import ClientError, ClientSession, ClientTimeout
|
from aiohttp import ClientError, ClientSession, ClientTimeout
|
||||||
from asyncio import TimeoutError
|
from asyncio import TimeoutError
|
||||||
# from asyncio.exceptions import IncompleteReadError
|
# from asyncio.exceptions import IncompleteReadError
|
||||||
# from http.client import IncompleteRead
|
# from http.client import IncompleteRead
|
||||||
# from lxml import html
|
# from lxml import html
|
||||||
# from xml.etree.ElementTree import ElementTree, ParseError
|
# from xml.etree.ElementTree import ElementTree, ParseError
|
||||||
import requests
|
#import requests
|
||||||
import slixfeed.config as config
|
import slixfeed.config as config
|
||||||
from slixfeed.log import Logger
|
from slixfeed.log import Logger
|
||||||
|
# import urllib.request
|
||||||
|
# from urllib.error import HTTPError
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
|
|
||||||
|
@ -55,7 +58,6 @@ except:
|
||||||
"Package magnet2torrent was not found.\n"
|
"Package magnet2torrent was not found.\n"
|
||||||
"BitTorrent is disabled.")
|
"BitTorrent is disabled.")
|
||||||
|
|
||||||
|
|
||||||
# class Dat:
|
# class Dat:
|
||||||
# async def dat():
|
# async def dat():
|
||||||
|
|
||||||
|
@ -68,52 +70,154 @@ except:
|
||||||
# class Gopher:
|
# class Gopher:
|
||||||
# async def gopher():
|
# async def gopher():
|
||||||
|
|
||||||
# class Http:
|
|
||||||
# async def http():
|
|
||||||
|
|
||||||
# class Ipfs:
|
# class Ipfs:
|
||||||
# async def ipfs():
|
# async def ipfs():
|
||||||
|
|
||||||
|
|
||||||
def http_response(url):
|
class Http:
|
||||||
"""
|
|
||||||
Download response headers.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
url : str
|
|
||||||
URL.
|
|
||||||
|
|
||||||
Returns
|
# def fetch_media(url, pathname):
|
||||||
-------
|
# try:
|
||||||
response: requests.models.Response
|
# urllib.request.urlretrieve(url, pathname)
|
||||||
HTTP Header Response.
|
# status = 1
|
||||||
|
# except HTTPError as e:
|
||||||
|
# logger.error(e)
|
||||||
|
# status = 0
|
||||||
|
# return status
|
||||||
|
|
||||||
Result would contain these:
|
|
||||||
response.encoding
|
async def fetch_headers(url):
|
||||||
response.headers
|
network_settings = config.get_values('settings.toml', 'network')
|
||||||
response.history
|
user_agent = (network_settings['user_agent'] or 'Slixfeed/0.1')
|
||||||
response.reason
|
headers = {'User-Agent': user_agent}
|
||||||
response.status_code
|
proxy = (network_settings['http_proxy'] or None)
|
||||||
response.url
|
timeout = ClientTimeout(total=10)
|
||||||
"""
|
async with ClientSession(headers=headers) as session:
|
||||||
user_agent = (
|
async with session.get(url, proxy=proxy,
|
||||||
config.get_value(
|
# proxy_auth=(proxy_username, proxy_password),
|
||||||
"settings", "Network", "user_agent")
|
timeout=timeout
|
||||||
) or 'Slixfeed/0.1'
|
) as response:
|
||||||
headers = {
|
headers = response.headers
|
||||||
"User-Agent": user_agent
|
return headers
|
||||||
}
|
# print("Headers for URL:", url)
|
||||||
try:
|
# for header_name, header_value in headers.items():
|
||||||
# Do not use HEAD request because it appears that too many sites would
|
# print(f"{header_name}: {header_value}")
|
||||||
# deny it.
|
|
||||||
# response = requests.head(url, headers=headers, allow_redirects=True)
|
|
||||||
response = requests.get(url, headers=headers, allow_redirects=True)
|
# TODO Write file to disk. Consider aiofiles
|
||||||
except Exception as e:
|
async def fetch_media(url, pathname):
|
||||||
logger.warning('Error in HTTP response')
|
"""
|
||||||
logger.error(e)
|
Download media content of given URL.
|
||||||
response = None
|
|
||||||
return response
|
Parameters
|
||||||
|
----------
|
||||||
|
url : str
|
||||||
|
URL.
|
||||||
|
pathname : list
|
||||||
|
Pathname (including filename) to save content to.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
msg: list or str
|
||||||
|
Document or error message.
|
||||||
|
"""
|
||||||
|
network_settings = config.get_values('settings.toml', 'network')
|
||||||
|
user_agent = (network_settings['user_agent'] or 'Slixfeed/0.1')
|
||||||
|
headers = {'User-Agent': user_agent}
|
||||||
|
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:
|
||||||
|
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(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
|
||||||
|
|
||||||
|
|
||||||
async def http(url):
|
async def http(url):
|
||||||
|
|
|
@ -19,6 +19,8 @@ import logging
|
||||||
|
|
||||||
class Logger:
|
class Logger:
|
||||||
|
|
||||||
|
def set_logging_level(level):
|
||||||
|
logging.basicConfig(level)
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.logger = logging.getLogger(name)
|
self.logger = logging.getLogger(name)
|
||||||
|
@ -58,4 +60,5 @@ class Message:
|
||||||
def printer(text):
|
def printer(text):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
current_time = now.strftime("%H:%M:%S")
|
current_time = now.strftime("%H:%M:%S")
|
||||||
print('{} {}'.format(current_time, text), end='\r')
|
# print('{} {}'.format(current_time, text), end='\r')
|
||||||
|
print('{} {}'.format(current_time, text))
|
||||||
|
|
|
@ -366,7 +366,7 @@ def create_tables(db_file):
|
||||||
id INTEGER NOT NULL,
|
id INTEGER NOT NULL,
|
||||||
feed_id INTEGER NOT NULL,
|
feed_id INTEGER NOT NULL,
|
||||||
tag_id INTEGER NOT NULL,
|
tag_id INTEGER NOT NULL,
|
||||||
FOREIGN KEY ("feed_id") REFERENCES "feeds" ("id")
|
FOREIGN KEY ("feed_id") REFERENCES "feeds_properties" ("id")
|
||||||
ON UPDATE CASCADE
|
ON UPDATE CASCADE
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
FOREIGN KEY ("tag_id") REFERENCES "tags" ("id")
|
FOREIGN KEY ("tag_id") REFERENCES "tags" ("id")
|
||||||
|
@ -2762,6 +2762,39 @@ def get_active_feeds_url(db_file):
|
||||||
return result
|
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):
|
def get_tags(db_file):
|
||||||
"""
|
"""
|
||||||
Query table tags and list items.
|
Query table tags and list items.
|
||||||
|
@ -2999,7 +3032,7 @@ def check_entry_exist(db_file, feed_id, identifier=None, title=None, link=None,
|
||||||
"""
|
"""
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM entries_properties
|
FROM entries_properties
|
||||||
WHERE identifier = :identifier and feed_id = :feed_id
|
WHERE identifier = :identifier AND feed_id = :feed_id
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
par = {
|
par = {
|
||||||
|
|
|
@ -348,7 +348,7 @@ class Feed:
|
||||||
if new_entries:
|
if new_entries:
|
||||||
await sqlite.add_entries_and_update_feed_state(
|
await sqlite.add_entries_and_update_feed_state(
|
||||||
db_file, feed_id, new_entries)
|
db_file, feed_id, new_entries)
|
||||||
old = Config.get_setting_value(self.settings, jid_bare, 'old')
|
old = Config.get_setting_value(self, jid_bare, 'old')
|
||||||
if not old: await sqlite.mark_feed_as_read(db_file, feed_id)
|
if not old: await sqlite.mark_feed_as_read(db_file, feed_id)
|
||||||
result_final = {'link' : url,
|
result_final = {'link' : url,
|
||||||
'index' : feed_id,
|
'index' : feed_id,
|
||||||
|
@ -1276,9 +1276,9 @@ class FeedTask:
|
||||||
logger.info('Scanning for updates for JID {}'.format(jid_bare))
|
logger.info('Scanning for updates for JID {}'.format(jid_bare))
|
||||||
while True:
|
while True:
|
||||||
db_file = config.get_pathname_to_database(jid_bare)
|
db_file = config.get_pathname_to_database(jid_bare)
|
||||||
urls = sqlite.get_active_feeds_url(db_file)
|
urls = sqlite.get_active_feeds_url_sorted_by_last_scanned(db_file)
|
||||||
for url in urls:
|
for url in urls:
|
||||||
Message.printer('Scanning updates for URL {} ...'.format(url))
|
#Message.printer('Scanning updates for URL {} ...'.format(url))
|
||||||
url = url[0]
|
url = url[0]
|
||||||
# print('STA',url)
|
# print('STA',url)
|
||||||
|
|
||||||
|
@ -1335,7 +1335,7 @@ class FeedTask:
|
||||||
new_entries.extend([new_entry])
|
new_entries.extend([new_entry])
|
||||||
if new_entries:
|
if new_entries:
|
||||||
await sqlite.add_entries_and_update_feed_state(db_file, feed_id, new_entries)
|
await sqlite.add_entries_and_update_feed_state(db_file, feed_id, new_entries)
|
||||||
limit = Config.get_setting_value(self.settings, jid_bare, 'archive')
|
limit = Config.get_setting_value(self, jid_bare, 'archive')
|
||||||
ixs = sqlite.get_entries_id_of_feed(db_file, feed_id)
|
ixs = sqlite.get_entries_id_of_feed(db_file, feed_id)
|
||||||
ixs_invalid = {}
|
ixs_invalid = {}
|
||||||
for ix in ixs:
|
for ix in ixs:
|
||||||
|
@ -1360,8 +1360,8 @@ class FeedTask:
|
||||||
# TODO return number of archived entries and add if statement to run archive maintainence function
|
# TODO return number of archived entries and add if statement to run archive maintainence function
|
||||||
await sqlite.maintain_archive(db_file, limit)
|
await sqlite.maintain_archive(db_file, limit)
|
||||||
# await sqlite.process_invalid_entries(db_file, ixs)
|
# await sqlite.process_invalid_entries(db_file, ixs)
|
||||||
await asyncio.sleep(50)
|
await asyncio.sleep(60 * 2)
|
||||||
val = Config.get_setting_value(self.settings, jid_bare, 'check')
|
val = Config.get_setting_value(self, jid_bare, 'check')
|
||||||
await asyncio.sleep(60 * float(val))
|
await asyncio.sleep(60 * float(val))
|
||||||
# Schedule to call this function again in 90 minutes
|
# Schedule to call this function again in 90 minutes
|
||||||
# loop.call_at(
|
# loop.call_at(
|
||||||
|
@ -1370,10 +1370,18 @@ class FeedTask:
|
||||||
# self.check_updates(jid)
|
# 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):
|
def restart_task(self, jid_bare):
|
||||||
if jid_bare == self.boundjid.bare:
|
|
||||||
return
|
|
||||||
if jid_bare not in self.task_manager:
|
if jid_bare not in self.task_manager:
|
||||||
self.task_manager[jid_bare] = {}
|
self.task_manager[jid_bare] = {}
|
||||||
logger.info('Creating new task manager for JID {}'.format(jid_bare))
|
logger.info('Creating new task manager for JID {}'.format(jid_bare))
|
||||||
|
|
|
@ -67,6 +67,107 @@ except:
|
||||||
logger = Logger(__name__)
|
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(jid_bare):
|
||||||
|
"""
|
||||||
|
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 = Config.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_bare))
|
||||||
|
sqlite.create_tables(db_file)
|
||||||
|
return db_file
|
||||||
|
|
||||||
|
|
||||||
class DateAndTime:
|
class DateAndTime:
|
||||||
|
|
||||||
#https://feedparser.readthedocs.io/en/latest/date-parsing.html
|
#https://feedparser.readthedocs.io/en/latest/date-parsing.html
|
||||||
|
@ -90,6 +191,12 @@ class DateAndTime:
|
||||||
return date
|
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():
|
def current_date():
|
||||||
"""
|
"""
|
||||||
Print MM DD, YYYY (Weekday Time) timestamp.
|
Print MM DD, YYYY (Weekday Time) timestamp.
|
||||||
|
@ -226,6 +333,7 @@ class Html:
|
||||||
'//img[not('
|
'//img[not('
|
||||||
'contains(@src, "avatar") or '
|
'contains(@src, "avatar") or '
|
||||||
'contains(@src, "cc-by-sa") or '
|
'contains(@src, "cc-by-sa") or '
|
||||||
|
'contains(@src, "data:image/") or '
|
||||||
'contains(@src, "emoji") or '
|
'contains(@src, "emoji") or '
|
||||||
'contains(@src, "icon") or '
|
'contains(@src, "icon") or '
|
||||||
'contains(@src, "logo") or '
|
'contains(@src, "logo") or '
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
__version__ = '0.1.85'
|
__version__ = '0.1.100'
|
||||||
__version_info__ = (0, 1, 85)
|
__version_info__ = (0, 1, 100)
|
||||||
|
|
|
@ -24,9 +24,13 @@ TODO
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
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).
|
from random import randrange # pending_tasks: Use a list and read the first index (i.e. index 0).
|
||||||
import slixfeed.config as config
|
import slixfeed.config as config
|
||||||
from slixfeed.config import Config
|
from slixfeed.config import Config
|
||||||
|
import slixfeed.fetch as fetch
|
||||||
|
from slixfeed.fetch import Http
|
||||||
from slixfeed.log import Logger
|
from slixfeed.log import Logger
|
||||||
import slixfeed.sqlite as sqlite
|
import slixfeed.sqlite as sqlite
|
||||||
from slixfeed.syndication import FeedTask
|
from slixfeed.syndication import FeedTask
|
||||||
|
@ -37,9 +41,16 @@ from slixfeed.xmpp.presence import XmppPresence
|
||||||
from slixfeed.xmpp.status import XmppStatusTask
|
from slixfeed.xmpp.status import XmppStatusTask
|
||||||
from slixfeed.xmpp.upload import XmppUpload
|
from slixfeed.xmpp.upload import XmppUpload
|
||||||
from slixfeed.xmpp.utilities import XmppUtilities
|
from slixfeed.xmpp.utilities import XmppUtilities
|
||||||
|
from slixmpp import JID
|
||||||
|
from slixmpp.stanza import Message
|
||||||
import sys
|
import sys
|
||||||
import time
|
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__)
|
logger = Logger(__name__)
|
||||||
|
|
||||||
|
@ -55,7 +66,7 @@ logger = Logger(__name__)
|
||||||
class XmppChat:
|
class XmppChat:
|
||||||
|
|
||||||
|
|
||||||
async def process_message(self, message):
|
async def process_message(self, message: Message, allow_untrusted: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Process incoming message stanzas. Be aware that this also
|
Process incoming message stanzas. Be aware that this also
|
||||||
includes MUC messages and error messages. It is usually
|
includes MUC messages and error messages. It is usually
|
||||||
|
@ -69,30 +80,31 @@ class XmppChat:
|
||||||
for stanza objects and the Message stanza to see
|
for stanza objects and the Message stanza to see
|
||||||
how it may be used.
|
how it may be used.
|
||||||
"""
|
"""
|
||||||
if message['type'] in ('chat', 'groupchat', 'normal'):
|
message_from = message['from']
|
||||||
jid_bare = message['from'].bare
|
message_type = message['type']
|
||||||
|
if message_type in ('chat', 'groupchat', 'normal'):
|
||||||
|
jid_bare = message_from.bare
|
||||||
command = ' '.join(message['body'].split())
|
command = ' '.join(message['body'].split())
|
||||||
command_time_start = time.time()
|
command_time_start = time.time()
|
||||||
|
|
||||||
# if (message['type'] == 'groupchat' and
|
# if (message_type == 'groupchat' and
|
||||||
# message['muc']['nick'] == self.alias):
|
# message['muc']['nick'] == self.alias):
|
||||||
# return
|
# return
|
||||||
|
|
||||||
# FIXME Code repetition. See below.
|
# FIXME Code repetition. See below.
|
||||||
# TODO Check alias by nickname associated with conference
|
# TODO Check alias by nickname associated with conference
|
||||||
if message['type'] == 'groupchat':
|
if message_type == 'groupchat':
|
||||||
if (message['muc']['nick'] == self.alias):
|
alias = message['muc']['nick']
|
||||||
|
if (alias == self.alias):
|
||||||
return
|
return
|
||||||
jid_full = str(message['from'])
|
if not XmppUtilities.is_moderator(self, jid_bare, alias):
|
||||||
if not XmppUtilities.is_moderator(self, jid_bare, jid_full):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if message['type'] == 'groupchat':
|
|
||||||
# nick = message['from'][message['from'].index('/')+1:]
|
# nick = message['from'][message['from'].index('/')+1:]
|
||||||
# nick = str(message['from'])
|
# nick = str(message['from'])
|
||||||
# nick = nick[nick.index('/')+1:]
|
# nick = nick[nick.index('/')+1:]
|
||||||
if (message['muc']['nick'] == self.alias or
|
alias_of_slixfeed = XmppUtilities.get_self_alias(self, jid_bare)
|
||||||
not message['body'].startswith('!')):
|
if (alias == self.alias or
|
||||||
|
not message['body'].startswith(alias_of_slixfeed)):
|
||||||
return
|
return
|
||||||
# token = await initdb(
|
# token = await initdb(
|
||||||
# jid_bare,
|
# jid_bare,
|
||||||
|
@ -109,8 +121,7 @@ class XmppChat:
|
||||||
# if nick not in operator:
|
# if nick not in operator:
|
||||||
# return
|
# return
|
||||||
# approved = False
|
# approved = False
|
||||||
jid_full = str(message['from'])
|
if not XmppUtilities.is_moderator(self, jid_bare, alias):
|
||||||
if not XmppUtilities.is_moderator(self, jid_bare, jid_full):
|
|
||||||
return
|
return
|
||||||
# if role == 'moderator':
|
# if role == 'moderator':
|
||||||
# approved = True
|
# approved = True
|
||||||
|
@ -140,17 +151,33 @@ class XmppChat:
|
||||||
|
|
||||||
# await compose.message(self, jid_bare, message)
|
# await compose.message(self, jid_bare, message)
|
||||||
|
|
||||||
if message['type'] == 'groupchat':
|
if self.omemo_present and self['xep_0384'].is_encrypted(message):
|
||||||
command = command[1:]
|
command, omemo_decrypted = await XmppOmemo.decrypt(
|
||||||
|
self, message)
|
||||||
|
else:
|
||||||
|
omemo_decrypted = None
|
||||||
|
|
||||||
|
if message_type == 'groupchat':
|
||||||
|
# Adding one to the length because of
|
||||||
|
# assumption that a comma or a dot is added
|
||||||
|
alias_of_slixfeed_length = len(alias_of_slixfeed) + 1
|
||||||
|
command = command[alias_of_slixfeed_length:].lstrip()
|
||||||
|
if isinstance(command, Message): command = command['body']
|
||||||
|
|
||||||
command_lowercase = command.lower()
|
command_lowercase = command.lower()
|
||||||
|
|
||||||
logger.debug([str(message['from']), ':', command])
|
# 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])
|
||||||
|
|
||||||
# Support private message via groupchat
|
# Support private message via groupchat
|
||||||
# See https://codeberg.org/poezio/slixmpp/issues/3506
|
# See https://codeberg.org/poezio/slixmpp/issues/3506
|
||||||
if message['type'] == 'chat' and message.get_plugin('muc', check=True):
|
if message_type == 'chat' and message.get_plugin('muc', check=True):
|
||||||
# jid_bare = message['from'].bare
|
# jid_bare = message_from.bare
|
||||||
jid_full = str(message['from'])
|
jid_full = message_from.full
|
||||||
if (jid_bare == jid_full[:jid_full.index('/')]):
|
if (jid_bare == jid_full[:jid_full.index('/')]):
|
||||||
# TODO Count and alert of MUC-PM attempts
|
# TODO Count and alert of MUC-PM attempts
|
||||||
return
|
return
|
||||||
|
@ -209,15 +236,15 @@ class XmppChat:
|
||||||
case _ if command_lowercase in ['greetings', 'hallo', 'hello',
|
case _ if command_lowercase in ['greetings', 'hallo', 'hello',
|
||||||
'hey', 'hi', 'hola', 'holla',
|
'hey', 'hi', 'hola', 'holla',
|
||||||
'hollo']:
|
'hollo']:
|
||||||
response = ('Greeting! My name is {}.\n'
|
response = ('Greeting. My name is {}.\n'
|
||||||
'I am an RSS News Bot.\n'
|
'I am an Atom/RSS News Bot.\n'
|
||||||
'Send "help" for further instructions.\n'
|
'Send "help" for further instructions.\n'
|
||||||
.format(self.alias))
|
.format(self.alias))
|
||||||
case _ if command_lowercase.startswith('add'):
|
case _ if command_lowercase.startswith('add'):
|
||||||
command = command[4:]
|
command = command[4:]
|
||||||
url = command.split(' ')[0]
|
url = command.split(' ')[0]
|
||||||
title = ' '.join(command.split(' ')[1:])
|
title = ' '.join(command.split(' ')[1:])
|
||||||
response = XmppCommands.feed_add(
|
response = await XmppCommands.feed_add(
|
||||||
url, db_file, jid_bare, title)
|
url, db_file, jid_bare, title)
|
||||||
case _ if command_lowercase.startswith('allow +'):
|
case _ if command_lowercase.startswith('allow +'):
|
||||||
val = command[7:]
|
val = command[7:]
|
||||||
|
@ -327,15 +354,25 @@ class XmppChat:
|
||||||
# self.pending_tasks[jid_bare][self.pending_tasks_counter] = status_message
|
# self.pending_tasks[jid_bare][self.pending_tasks_counter] = status_message
|
||||||
XmppPresence.send(self, jid_bare, status_message,
|
XmppPresence.send(self, jid_bare, status_message,
|
||||||
status_type=status_type)
|
status_type=status_type)
|
||||||
filename, response = XmppCommands.export_feeds(
|
pathname, response = XmppCommands.export_feeds(
|
||||||
jid_bare, ext)
|
jid_bare, ext)
|
||||||
url = await XmppUpload.start(self, jid_bare, filename)
|
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)
|
||||||
# response = (
|
# response = (
|
||||||
# 'Feeds exported successfully to {}.\n{}'
|
# 'Feeds exported successfully to {}.\n{}'
|
||||||
# ).format(ex, url)
|
# ).format(ex, url)
|
||||||
# XmppMessage.send_oob_reply_message(message, url, response)
|
# XmppMessage.send_oob_reply_message(message, url, response)
|
||||||
chat_type = await XmppUtilities.get_chat_type(self, jid_bare)
|
if url:
|
||||||
XmppMessage.send_oob(self, jid_bare, url, chat_type)
|
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.'
|
||||||
del self.pending_tasks[jid_bare][pending_tasks_num]
|
del self.pending_tasks[jid_bare][pending_tasks_num]
|
||||||
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
|
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
|
||||||
XmppStatusTask.restart_task(self, jid_bare)
|
XmppStatusTask.restart_task(self, jid_bare)
|
||||||
|
@ -353,7 +390,7 @@ class XmppChat:
|
||||||
response = (first_line + result +
|
response = (first_line + result +
|
||||||
'\n```\nTotal of {} feeds'.format(number))
|
'\n```\nTotal of {} feeds'.format(number))
|
||||||
case 'goodbye':
|
case 'goodbye':
|
||||||
if message['type'] == 'groupchat':
|
if message_type == 'groupchat':
|
||||||
await XmppCommands.muc_leave(self, jid_bare)
|
await XmppCommands.muc_leave(self, jid_bare)
|
||||||
else:
|
else:
|
||||||
response = 'This command is valid in groupchat only.'
|
response = 'This command is valid in groupchat only.'
|
||||||
|
@ -378,18 +415,18 @@ class XmppChat:
|
||||||
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
|
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
|
||||||
XmppStatusTask.restart_task(self, jid_bare)
|
XmppStatusTask.restart_task(self, jid_bare)
|
||||||
case _ if command_lowercase.startswith('pubsub list'):
|
case _ if command_lowercase.startswith('pubsub list'):
|
||||||
jid = command[12:]
|
jid_full_pubsub = command[12:]
|
||||||
response = 'List of nodes for {}:\n```\n'.format(jid)
|
response = 'List of nodes for {}:\n```\n'.format(jid_full_pubsub)
|
||||||
response = await XmppCommands.pubsub_list(self, jid)
|
response = await XmppCommands.pubsub_list(self, jid_full_pubsub)
|
||||||
response += '```'
|
response += '```'
|
||||||
case _ if command_lowercase.startswith('pubsub send'):
|
case _ if command_lowercase.startswith('pubsub send'):
|
||||||
if XmppUtilities.is_operator(self, jid_bare):
|
if XmppUtilities.is_operator(self, jid_bare):
|
||||||
info = command[12:]
|
info = command[12:]
|
||||||
info = info.split(' ')
|
info = info.split(' ')
|
||||||
jid = info[0]
|
jid_full_pubsub = info[0]
|
||||||
# num = int(info[1])
|
# num = int(info[1])
|
||||||
if jid:
|
if jid_full_pubsub:
|
||||||
response = XmppCommands.pubsub_send(self, info, jid)
|
response = XmppCommands.pubsub_send(self, info, jid_full_pubsub)
|
||||||
else:
|
else:
|
||||||
response = ('This action is restricted. '
|
response = ('This action is restricted. '
|
||||||
'Type: sending news to PubSub.')
|
'Type: sending news to PubSub.')
|
||||||
|
@ -453,6 +490,13 @@ class XmppChat:
|
||||||
self, jid_bare, db_file)
|
self, jid_bare, db_file)
|
||||||
case _ if command_lowercase.startswith('next'):
|
case _ if command_lowercase.startswith('next'):
|
||||||
num = command[5:]
|
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)
|
await XmppChatAction.send_unread_items(self, jid_bare, num)
|
||||||
XmppStatusTask.restart_task(self, jid_bare)
|
XmppStatusTask.restart_task(self, jid_bare)
|
||||||
case _ if command_lowercase.startswith('node delete'):
|
case _ if command_lowercase.startswith('node delete'):
|
||||||
|
@ -474,6 +518,12 @@ class XmppChat:
|
||||||
case 'old':
|
case 'old':
|
||||||
response = await XmppCommands.set_old_on(
|
response = await XmppCommands.set_old_on(
|
||||||
self, jid_bare, db_file)
|
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':
|
case 'options':
|
||||||
response = 'Options:\n```'
|
response = 'Options:\n```'
|
||||||
response += XmppCommands.print_options(self, jid_bare)
|
response += XmppCommands.print_options(self, jid_bare)
|
||||||
|
@ -548,13 +598,13 @@ class XmppChat:
|
||||||
response = XmppCommands.search_items(db_file, query)
|
response = XmppCommands.search_items(db_file, query)
|
||||||
case 'start':
|
case 'start':
|
||||||
status_type = 'available'
|
status_type = 'available'
|
||||||
status_message = '📫️ Welcome back!'
|
status_message = '📫️ Welcome back.'
|
||||||
XmppPresence.send(self, jid_bare, status_message,
|
XmppPresence.send(self, jid_bare, status_message,
|
||||||
status_type=status_type)
|
status_type=status_type)
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
tasks = (FeedTask, XmppChatTask, XmppStatusTask)
|
callbacks = (FeedTask, XmppChatTask, XmppStatusTask)
|
||||||
response = await XmppCommands.scheduler_start(
|
response = await XmppCommands.scheduler_start(
|
||||||
self, db_file, jid_bare, tasks)
|
self, db_file, jid_bare, callbacks)
|
||||||
case 'stats':
|
case 'stats':
|
||||||
response = XmppCommands.print_statistics(db_file)
|
response = XmppCommands.print_statistics(db_file)
|
||||||
case 'stop':
|
case 'stop':
|
||||||
|
@ -581,8 +631,20 @@ class XmppChat:
|
||||||
command_time_finish = time.time()
|
command_time_finish = time.time()
|
||||||
command_time_total = command_time_finish - command_time_start
|
command_time_total = command_time_finish - command_time_start
|
||||||
command_time_total = round(command_time_total, 3)
|
command_time_total = round(command_time_total, 3)
|
||||||
if response: XmppMessage.send_reply(self, message, response)
|
if response:
|
||||||
if Config.get_setting_value(self.settings, jid_bare, 'finished'):
|
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 = 'Finished. Total time: {}s'.format(command_time_total)
|
response_finished = 'Finished. Total time: {}s'.format(command_time_total)
|
||||||
XmppMessage.send_reply(self, message, response_finished)
|
XmppMessage.send_reply(self, message, response_finished)
|
||||||
|
|
||||||
|
@ -610,13 +672,13 @@ class XmppChat:
|
||||||
class XmppChatAction:
|
class XmppChatAction:
|
||||||
|
|
||||||
|
|
||||||
async def send_unread_items(self, jid_bare, num=None):
|
async def send_unread_items(self, jid_bare, num: Optional[int] = None):
|
||||||
"""
|
"""
|
||||||
Send news items as messages.
|
Send news items as messages.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
jid : str
|
jid_bare : str
|
||||||
Jabber ID.
|
Jabber ID.
|
||||||
num : str, optional
|
num : str, optional
|
||||||
Number. The default is None.
|
Number. The default is None.
|
||||||
|
@ -624,14 +686,17 @@ class XmppChatAction:
|
||||||
function_name = sys._getframe().f_code.co_name
|
function_name = sys._getframe().f_code.co_name
|
||||||
logger.debug('{}: jid: {} num: {}'.format(function_name, jid_bare, num))
|
logger.debug('{}: jid: {} num: {}'.format(function_name, jid_bare, num))
|
||||||
db_file = config.get_pathname_to_database(jid_bare)
|
db_file = config.get_pathname_to_database(jid_bare)
|
||||||
show_media = Config.get_setting_value(self.settings, jid_bare, 'media')
|
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')
|
||||||
if not num:
|
if not num:
|
||||||
num = Config.get_setting_value(self.settings, jid_bare, 'quantum')
|
num = Config.get_setting_value(self, jid_bare, 'quantum')
|
||||||
else:
|
else:
|
||||||
num = int(num)
|
num = int(num)
|
||||||
results = sqlite.get_unread_entries(db_file, num)
|
results = sqlite.get_unread_entries(db_file, num)
|
||||||
news_digest = ''
|
news_digest = ''
|
||||||
media = None
|
media_url = None
|
||||||
chat_type = await XmppUtilities.get_chat_type(self, jid_bare)
|
chat_type = await XmppUtilities.get_chat_type(self, jid_bare)
|
||||||
for result in results:
|
for result in results:
|
||||||
ix = result[0]
|
ix = result[0]
|
||||||
|
@ -658,20 +723,93 @@ class XmppChatAction:
|
||||||
# elif enclosure:
|
# elif enclosure:
|
||||||
if show_media:
|
if show_media:
|
||||||
if enclosure:
|
if enclosure:
|
||||||
media = enclosure
|
media_url = enclosure
|
||||||
else:
|
else:
|
||||||
media = await Html.extract_image_from_html(url)
|
media_url = await Html.extract_image_from_html(url)
|
||||||
|
try:
|
||||||
|
http_headers = await Http.fetch_headers(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 and news_digest:
|
if media_url and news_digest:
|
||||||
# Send textual message
|
if self.omemo_present and encrypt_omemo:
|
||||||
XmppMessage.send(self, jid_bare, news_digest, chat_type)
|
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)
|
||||||
news_digest = ''
|
news_digest = ''
|
||||||
# Send media
|
# Send media
|
||||||
XmppMessage.send_oob(self, jid_bare, media, chat_type)
|
if self.omemo_present and encrypt_omemo:
|
||||||
media = None
|
cache_dir = config.get_default_cache_directory()
|
||||||
|
# if not media_url.startswith('data:'):
|
||||||
|
filename = media_url.split('/').pop().split('?')[0]
|
||||||
|
if not filename: breakpoint()
|
||||||
|
pathname = os.path.join(cache_dir, filename)
|
||||||
|
# http_response = await Http.response(media_url)
|
||||||
|
http_headers = await Http.fetch_headers(media_url)
|
||||||
|
if ('Content-Length' in http_headers and
|
||||||
|
int(http_headers['Content-Length']) < 3000000):
|
||||||
|
status = await Http.fetch_media(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(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
|
||||||
|
|
||||||
if news_digest:
|
if news_digest:
|
||||||
XmppMessage.send(self, jid_bare, news_digest, chat_type)
|
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)
|
||||||
# TODO Add while loop to assure delivery.
|
# TODO Add while loop to assure delivery.
|
||||||
# print(await current_time(), ">>> ACT send_message",jid)
|
# print(await current_time(), ">>> ACT send_message",jid)
|
||||||
# NOTE Do we need "if statement"? See NOTE at is_muc.
|
# NOTE Do we need "if statement"? See NOTE at is_muc.
|
||||||
|
@ -756,7 +894,7 @@ class XmppChatAction:
|
||||||
summary = summary.replace(' ', ' ')
|
summary = summary.replace(' ', ' ')
|
||||||
# summary = summary.replace(' ', ' ')
|
# summary = summary.replace(' ', ' ')
|
||||||
summary = ' '.join(summary.split())
|
summary = ' '.join(summary.split())
|
||||||
length = Config.get_setting_value(self.settings, jid, 'length')
|
length = Config.get_setting_value(self, jid, 'length')
|
||||||
length = int(length)
|
length = int(length)
|
||||||
summary = summary[:length] + " […]"
|
summary = summary[:length] + " […]"
|
||||||
# summary = summary.strip().split('\n')
|
# summary = summary.strip().split('\n')
|
||||||
|
@ -770,7 +908,7 @@ class XmppChatAction:
|
||||||
feed_id = result[4]
|
feed_id = result[4]
|
||||||
# news_item = ("\n{}\n{}\n{} [{}]\n").format(str(title), str(link),
|
# news_item = ("\n{}\n{}\n{} [{}]\n").format(str(title), str(link),
|
||||||
# str(feed_title), str(ix))
|
# str(feed_title), str(ix))
|
||||||
formatting = Config.get_setting_value(self.settings, jid, 'formatting')
|
formatting = Config.get_setting_value(self, jid, 'formatting')
|
||||||
news_item = formatting.format(feed_title=feed_title,
|
news_item = formatting.format(feed_title=feed_title,
|
||||||
title=title,
|
title=title,
|
||||||
summary=summary,
|
summary=summary,
|
||||||
|
@ -787,9 +925,9 @@ class XmppChatTask:
|
||||||
async def task_message(self, jid_bare):
|
async def task_message(self, jid_bare):
|
||||||
db_file = config.get_pathname_to_database(jid_bare)
|
db_file = config.get_pathname_to_database(jid_bare)
|
||||||
if jid_bare not in self.settings:
|
if jid_bare not in self.settings:
|
||||||
Config.add_settings_jid(self.settings, jid_bare, db_file)
|
Config.add_settings_jid(self, jid_bare, db_file)
|
||||||
while True:
|
while True:
|
||||||
update_interval = Config.get_setting_value(self.settings, jid_bare, 'interval')
|
update_interval = Config.get_setting_value(self, jid_bare, 'interval')
|
||||||
update_interval = 60 * int(update_interval)
|
update_interval = 60 * int(update_interval)
|
||||||
last_update_time = sqlite.get_last_update_time(db_file)
|
last_update_time = sqlite.get_last_update_time(db_file)
|
||||||
if last_update_time:
|
if last_update_time:
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -174,10 +174,8 @@ class XmppCommands:
|
||||||
# the look into function "check_updates" of module "task".
|
# the look into function "check_updates" of module "task".
|
||||||
# await action.scan(self, jid_bare, db_file, url)
|
# await action.scan(self, jid_bare, db_file, url)
|
||||||
# if jid_bare not in self.settings:
|
# if jid_bare not in self.settings:
|
||||||
# Config.add_settings_jid(self.settings, jid_bare,
|
# Config.add_settings_jid(self, jid_bare, db_file)
|
||||||
# db_file)
|
# old = Config.get_setting_value(self, jid_bare, 'old')
|
||||||
# old = Config.get_setting_value(self.settings, jid_bare,
|
|
||||||
# 'old')
|
|
||||||
# if old:
|
# if old:
|
||||||
# # task.clean_tasks_xmpp_chat(self, jid_bare, ['status'])
|
# # task.clean_tasks_xmpp_chat(self, jid_bare, ['status'])
|
||||||
# # await send_status(jid)
|
# # await send_status(jid)
|
||||||
|
@ -234,8 +232,7 @@ class XmppCommands:
|
||||||
|
|
||||||
|
|
||||||
def get_archive(self, jid_bare):
|
def get_archive(self, jid_bare):
|
||||||
result = Config.get_setting_value(
|
result = Config.get_setting_value(self, jid_bare, 'archive')
|
||||||
self.settings, jid_bare, 'archive')
|
|
||||||
message = str(result)
|
message = str(result)
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
@ -246,10 +243,9 @@ class XmppCommands:
|
||||||
if val_new > 500:
|
if val_new > 500:
|
||||||
message = 'Value may not be greater than 500.'
|
message = 'Value may not be greater than 500.'
|
||||||
else:
|
else:
|
||||||
val_old = Config.get_setting_value(
|
val_old = Config.get_setting_value(self, jid_bare, 'archive')
|
||||||
self.settings, jid_bare, 'archive')
|
|
||||||
await Config.set_setting_value(
|
await Config.set_setting_value(
|
||||||
self.settings, jid_bare, db_file, 'archive', val_new)
|
self, jid_bare, db_file, 'archive', val_new)
|
||||||
message = ('Maximum archived items has been set to {} (was: {}).'
|
message = ('Maximum archived items has been set to {} (was: {}).'
|
||||||
.format(val_new, val_old))
|
.format(val_new, val_old))
|
||||||
except:
|
except:
|
||||||
|
@ -334,9 +330,9 @@ class XmppCommands:
|
||||||
|
|
||||||
|
|
||||||
def export_feeds(jid_bare, ext):
|
def export_feeds(jid_bare, ext):
|
||||||
filename = Feed.export_feeds(jid_bare, ext)
|
pathname = Feed.export_feeds(jid_bare, ext)
|
||||||
message = 'Feeds successfuly exported to {}.'.format(ext)
|
message = 'Feeds successfuly exported to {}.'.format(ext)
|
||||||
return filename, message
|
return pathname, message
|
||||||
|
|
||||||
|
|
||||||
def fetch_gemini():
|
def fetch_gemini():
|
||||||
|
@ -550,8 +546,7 @@ class XmppCommands:
|
||||||
|
|
||||||
|
|
||||||
def get_interval(self, jid_bare):
|
def get_interval(self, jid_bare):
|
||||||
result = Config.get_setting_value(
|
result = Config.get_setting_value(self, jid_bare, 'interval')
|
||||||
self.settings, jid_bare, 'interval')
|
|
||||||
message = str(result)
|
message = str(result)
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
@ -559,10 +554,9 @@ class XmppCommands:
|
||||||
async def set_interval(self, db_file, jid_bare, val):
|
async def set_interval(self, db_file, jid_bare, val):
|
||||||
try:
|
try:
|
||||||
val_new = int(val)
|
val_new = int(val)
|
||||||
val_old = Config.get_setting_value(
|
val_old = Config.get_setting_value(self, jid_bare, 'interval')
|
||||||
self.settings, jid_bare, 'interval')
|
|
||||||
await Config.set_setting_value(
|
await Config.set_setting_value(
|
||||||
self.settings, jid_bare, db_file, 'interval', val_new)
|
self, jid_bare, db_file, 'interval', val_new)
|
||||||
message = ('Updates will be sent every {} minutes '
|
message = ('Updates will be sent every {} minutes '
|
||||||
'(was: {}).'.format(val_new, val_old))
|
'(was: {}).'.format(val_new, val_old))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -596,8 +590,7 @@ class XmppCommands:
|
||||||
|
|
||||||
|
|
||||||
def get_length(self, jid_bare):
|
def get_length(self, jid_bare):
|
||||||
result = Config.get_setting_value(
|
result = Config.get_setting_value(self, jid_bare, 'length')
|
||||||
self.settings, jid_bare, 'length')
|
|
||||||
result = str(result)
|
result = str(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -605,10 +598,9 @@ class XmppCommands:
|
||||||
async def set_length(self, db_file, jid_bare, val):
|
async def set_length(self, db_file, jid_bare, val):
|
||||||
try:
|
try:
|
||||||
val_new = int(val)
|
val_new = int(val)
|
||||||
val_old = Config.get_setting_value(
|
val_old = Config.get_setting_value(self, jid_bare, 'length')
|
||||||
self.settings, jid_bare, 'length')
|
|
||||||
await Config.set_setting_value(
|
await Config.set_setting_value(
|
||||||
self.settings, jid_bare, db_file, 'length', val_new)
|
self, jid_bare, db_file, 'length', val_new)
|
||||||
if not val_new: # i.e. val_new == 0
|
if not val_new: # i.e. val_new == 0
|
||||||
# TODO Add action to disable limit
|
# TODO Add action to disable limit
|
||||||
message = ('Summary length limit is disabled '
|
message = ('Summary length limit is disabled '
|
||||||
|
@ -625,29 +617,41 @@ class XmppCommands:
|
||||||
|
|
||||||
|
|
||||||
async def set_media_off(self, jid_bare, db_file):
|
async def set_media_off(self, jid_bare, db_file):
|
||||||
await Config.set_setting_value(self.settings, jid_bare, db_file, 'media', 0)
|
await Config.set_setting_value(self, jid_bare, db_file, 'media', 0)
|
||||||
message = 'Media is disabled.'
|
message = 'Media is disabled.'
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
||||||
async def set_media_on(self, jid_bare, db_file):
|
async def set_media_on(self, jid_bare, db_file):
|
||||||
await Config.set_setting_value(self.settings, jid_bare, db_file, 'media', 1)
|
await Config.set_setting_value(self, jid_bare, db_file, 'media', 1)
|
||||||
message = 'Media is enabled.'
|
message = 'Media is enabled.'
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
||||||
async def set_old_off(self, jid_bare, db_file):
|
async def set_old_off(self, jid_bare, db_file):
|
||||||
await Config.set_setting_value(self.settings, jid_bare, db_file, 'old', 0)
|
await Config.set_setting_value(self, jid_bare, db_file, 'old', 0)
|
||||||
message = 'Only new items of newly added feeds be delivered.'
|
message = 'Only new items of newly added feeds be delivered.'
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
||||||
async def set_old_on(self, jid_bare, db_file):
|
async def set_old_on(self, jid_bare, db_file):
|
||||||
await Config.set_setting_value(self.settings, jid_bare, db_file, 'old', 1)
|
await Config.set_setting_value(self, jid_bare, db_file, 'old', 1)
|
||||||
message = 'All items of newly added feeds be delivered.'
|
message = 'All items of newly added feeds be delivered.'
|
||||||
return message
|
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):
|
def node_delete(self, info):
|
||||||
info = info.split(' ')
|
info = info.split(' ')
|
||||||
if len(info) > 2:
|
if len(info) > 2:
|
||||||
|
@ -687,8 +691,8 @@ class XmppCommands:
|
||||||
def print_options(self, jid_bare):
|
def print_options(self, jid_bare):
|
||||||
message = ''
|
message = ''
|
||||||
for key in self.settings[jid_bare]:
|
for key in self.settings[jid_bare]:
|
||||||
val = Config.get_setting_value(self.settings, 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, jid_bare, key)
|
||||||
steps = 11 - len(key)
|
steps = 11 - len(key)
|
||||||
pulse = ''
|
pulse = ''
|
||||||
for step in range(steps):
|
for step in range(steps):
|
||||||
|
@ -698,8 +702,7 @@ class XmppCommands:
|
||||||
|
|
||||||
|
|
||||||
def get_quantum(self, jid_bare):
|
def get_quantum(self, jid_bare):
|
||||||
result = Config.get_setting_value(
|
result = Config.get_setting_value(self, jid_bare, 'quantum')
|
||||||
self.settings, jid_bare, 'quantum')
|
|
||||||
message = str(result)
|
message = str(result)
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
@ -707,14 +710,13 @@ class XmppCommands:
|
||||||
async def set_quantum(self, db_file, jid_bare, val):
|
async def set_quantum(self, db_file, jid_bare, val):
|
||||||
try:
|
try:
|
||||||
val_new = int(val)
|
val_new = int(val)
|
||||||
val_old = Config.get_setting_value(
|
val_old = Config.get_setting_value(self, jid_bare, 'quantum')
|
||||||
self.settings, jid_bare, 'quantum')
|
|
||||||
# response = (
|
# response = (
|
||||||
# 'Every update will contain {} news items.'
|
# 'Every update will contain {} news items.'
|
||||||
# ).format(response)
|
# ).format(response)
|
||||||
db_file = config.get_pathname_to_database(jid_bare)
|
db_file = config.get_pathname_to_database(jid_bare)
|
||||||
await Config.set_setting_value(self.settings, jid_bare,
|
await Config.set_setting_value(
|
||||||
db_file, 'quantum', val_new)
|
self, jid_bare, db_file, 'quantum', val_new)
|
||||||
message = ('Next update will contain {} news items (was: {}).'
|
message = ('Next update will contain {} news items (was: {}).'
|
||||||
.format(val_new, val_old))
|
.format(val_new, val_old))
|
||||||
except:
|
except:
|
||||||
|
@ -958,17 +960,16 @@ class XmppCommands:
|
||||||
|
|
||||||
# Tasks are classes which are passed to this function
|
# Tasks are classes which are passed to this function
|
||||||
# On an occasion in which they would have returned, variable "tasks" might be called "callback"
|
# On an occasion in which they would have returned, variable "tasks" might be called "callback"
|
||||||
async def scheduler_start(self, db_file, jid_bare, tasks):
|
async def scheduler_start(self, db_file, jid_bare, callbacks):
|
||||||
await Config.set_setting_value(self.settings, jid_bare, db_file, 'enabled', 1)
|
await Config.set_setting_value(self, jid_bare, db_file, 'enabled', 1)
|
||||||
for task in tasks:
|
for callback in callbacks:
|
||||||
task.restart_task(self, jid_bare)
|
callback.restart_task(self, jid_bare)
|
||||||
message = 'Updates are enabled.'
|
message = 'Updates are enabled.'
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
||||||
async def scheduler_stop(self, db_file, jid_bare):
|
async def scheduler_stop(self, db_file, jid_bare):
|
||||||
await Config.set_setting_value(
|
await Config.set_setting_value(self, jid_bare, db_file, 'enabled', 0)
|
||||||
self.settings, jid_bare, db_file, 'enabled', 0)
|
|
||||||
for task in ('interval', 'status'):
|
for task in ('interval', 'status'):
|
||||||
if (jid_bare in self.task_manager and
|
if (jid_bare in self.task_manager and
|
||||||
task in self.task_manager[jid_bare]):
|
task in self.task_manager[jid_bare]):
|
||||||
|
|
343
slixfeed/xmpp/encryption.py
Normal file
343
slixfeed/xmpp/encryption.py
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
#!/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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
omemo_dir = Data.get_pathname_to_omemo_directory()
|
||||||
|
JSON_FILE = os.path.join(omemo_dir, 'omemo.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,21 +34,31 @@ class XmppGroupchat:
|
||||||
'bookmark {}'.format(bookmark['name']))
|
'bookmark {}'.format(bookmark['name']))
|
||||||
alias = bookmark["nick"]
|
alias = bookmark["nick"]
|
||||||
muc_jid = bookmark["jid"]
|
muc_jid = bookmark["jid"]
|
||||||
Message.printer('Joining to MUC {} ...'.format(muc_jid))
|
# Message.printer('Joining to MUC {} ...'.format(muc_jid))
|
||||||
|
print('Joining to MUC {} ...'.format(muc_jid))
|
||||||
result = await XmppMuc.join(self, muc_jid, alias)
|
result = await XmppMuc.join(self, muc_jid, alias)
|
||||||
if result == 'ban':
|
match result:
|
||||||
await XmppBookmark.remove(self, muc_jid)
|
case 'ban':
|
||||||
logger.warning('{} is banned from {}'.format(self.alias, muc_jid))
|
await XmppBookmark.remove(self, muc_jid)
|
||||||
logger.warning('Groupchat {} has been removed from bookmarks'
|
logger.warning('{} is banned from {}'.format(self.alias, muc_jid))
|
||||||
.format(muc_jid))
|
logger.warning('Groupchat {} has been removed from bookmarks'
|
||||||
else:
|
.format(muc_jid))
|
||||||
logger.info('Autojoin groupchat\n'
|
case 'error':
|
||||||
'Name : {}\n'
|
logger.warning('An error has occured while attempting '
|
||||||
'JID : {}\n'
|
'to join to groupchat {}'
|
||||||
'Alias : {}\n'
|
.format(muc_jid))
|
||||||
.format(bookmark["name"],
|
case 'timeout':
|
||||||
bookmark["jid"],
|
logger.warning('Timeout has reached while attempting '
|
||||||
bookmark["nick"]))
|
'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"]))
|
||||||
elif not bookmark["jid"]:
|
elif not bookmark["jid"]:
|
||||||
logger.error('JID is missing for bookmark {}'
|
logger.error('JID is missing for bookmark {}'
|
||||||
.format(bookmark['name']))
|
.format(bookmark['name']))
|
||||||
|
|
|
@ -10,10 +10,13 @@ class XmppIQ:
|
||||||
|
|
||||||
async def send(self, iq):
|
async def send(self, iq):
|
||||||
try:
|
try:
|
||||||
await iq.send(timeout=15)
|
result = await iq.send(timeout=15)
|
||||||
except IqTimeout as e:
|
except IqTimeout as e:
|
||||||
logger.error('Error Timeout')
|
logger.error('Error Timeout')
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
|
result = e
|
||||||
except IqError as e:
|
except IqError as e:
|
||||||
logger.error('Error XmppIQ')
|
logger.error('Error XmppIQ')
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
|
result = e
|
||||||
|
return result
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from slixfeed.log import Logger
|
from slixfeed.log import Logger
|
||||||
|
from slixmpp import JID
|
||||||
import xml.sax.saxutils as saxutils
|
import xml.sax.saxutils as saxutils
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
|
@ -39,6 +40,46 @@ class XmppMessage:
|
||||||
mnick=self.alias)
|
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
|
# NOTE We might want to add more characters
|
||||||
# def escape_to_xml(raw_string):
|
# def escape_to_xml(raw_string):
|
||||||
# escape_map = {
|
# escape_map = {
|
||||||
|
|
|
@ -46,7 +46,7 @@ class XmppMuc:
|
||||||
# )
|
# )
|
||||||
logger.info('Joining groupchat\nJID : {}\n'.format(jid))
|
logger.info('Joining groupchat\nJID : {}\n'.format(jid))
|
||||||
jid_from = str(self.boundjid) if self.is_component else None
|
jid_from = str(self.boundjid) if self.is_component else None
|
||||||
if alias == None: self.alias
|
if not alias: alias = self.alias
|
||||||
try:
|
try:
|
||||||
await self.plugin['xep_0045'].join_muc_wait(jid,
|
await self.plugin['xep_0045'].join_muc_wait(jid,
|
||||||
alias,
|
alias,
|
||||||
|
|
|
@ -44,10 +44,10 @@ class XmppPubsub:
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
async def get_node_properties(self, jid, node):
|
async def get_node_properties(self, jid_bare, node):
|
||||||
config = await self.plugin['xep_0060'].get_node_config(jid, node)
|
config = await self.plugin['xep_0060'].get_node_config(jid_bare, node)
|
||||||
subscriptions = await self.plugin['xep_0060'].get_node_subscriptions(jid, node)
|
subscriptions = await self.plugin['xep_0060'].get_node_subscriptions(jid_bare, node)
|
||||||
affiliations = await self.plugin['xep_0060'].get_node_affiliations(jid, node)
|
affiliations = await self.plugin['xep_0060'].get_node_affiliations(jid_bare, node)
|
||||||
properties = {'config': config,
|
properties = {'config': config,
|
||||||
'subscriptions': subscriptions,
|
'subscriptions': subscriptions,
|
||||||
'affiliations': affiliations}
|
'affiliations': affiliations}
|
||||||
|
@ -55,49 +55,48 @@ class XmppPubsub:
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
|
|
||||||
async def get_node_configuration(self, jid, node_id):
|
|
||||||
node = await self.plugin['xep_0060'].get_node_config(jid, node_id)
|
async def get_node_configuration(self, jid_bare, node_id):
|
||||||
if not node:
|
node = await self.plugin['xep_0060'].get_node_config(jid_bare, node_id)
|
||||||
print('NODE CONFIG', node_id, str(node))
|
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
||||||
async def get_nodes(self, jid):
|
async def get_nodes(self, jid_bare):
|
||||||
nodes = await self.plugin['xep_0060'].get_nodes(jid)
|
nodes = await self.plugin['xep_0060'].get_nodes(jid_bare)
|
||||||
# 'self' would lead to slixmpp.jid.InvalidJID: idna validation failed:
|
# 'self' would lead to slixmpp.jid.InvalidJID: idna validation failed:
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
async def get_item(self, jid, node, item_id):
|
async def get_item(self, jid_bare, node, item_id):
|
||||||
item = await self.plugin['xep_0060'].get_item(jid, node, item_id)
|
item = await self.plugin['xep_0060'].get_item(jid_bare, node, item_id)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
async def get_items(self, jid, node):
|
async def get_items(self, jid_bare, node):
|
||||||
items = await self.plugin['xep_0060'].get_items(jid, node)
|
items = await self.plugin['xep_0060'].get_items(jid_bare, node)
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def delete_node(self, jid, node):
|
def delete_node(self, jid_bare, node):
|
||||||
jid_from = str(self.boundjid) if self.is_component else None
|
jid_from = self.boundjid.bare if self.is_component else None
|
||||||
self.plugin['xep_0060'].delete_node(jid, node, ifrom=jid_from)
|
self.plugin['xep_0060'].delete_node(jid_bare, node, ifrom=jid_from)
|
||||||
|
|
||||||
|
|
||||||
def purge_node(self, jid, node):
|
def purge_node(self, jid_bare, node):
|
||||||
jid_from = str(self.boundjid) if self.is_component else None
|
jid_from = self.boundjid.bare if self.is_component else None
|
||||||
self.plugin['xep_0060'].purge(jid, node, ifrom=jid_from)
|
self.plugin['xep_0060'].purge(jid_bare, node, ifrom=jid_from)
|
||||||
# iq = self.Iq(stype='set',
|
# iq = self.Iq(stype='set',
|
||||||
# sto=jid,
|
# sto=jid_bare,
|
||||||
# sfrom=jid_from)
|
# sfrom=jid_from)
|
||||||
# iq['pubsub']['purge']['node'] = node
|
# iq['pubsub']['purge']['node'] = node
|
||||||
# return iq
|
# return iq
|
||||||
|
|
||||||
|
|
||||||
# TODO Make use of var "xep" with match/case (XEP-0060, XEP-0277, XEP-0472)
|
# TODO Make use of var "xep" with match/case (XEP-0060, XEP-0277, XEP-0472)
|
||||||
def create_node(self, jid, node, xep ,title=None, subtitle=None):
|
def create_node(self, jid_bare, node, xep=None ,title=None, subtitle=None):
|
||||||
jid_from = str(self.boundjid) if self.is_component else None
|
jid_from = self.boundjid.bare if self.is_component else None
|
||||||
iq = self.Iq(stype='set',
|
iq = self.Iq(stype='set',
|
||||||
sto=jid,
|
sto=jid_bare,
|
||||||
sfrom=jid_from)
|
sfrom=jid_from)
|
||||||
iq['pubsub']['create']['node'] = node
|
iq['pubsub']['create']['node'] = node
|
||||||
form = iq['pubsub']['configure']['form']
|
form = iq['pubsub']['configure']['form']
|
||||||
|
@ -131,8 +130,8 @@ class XmppPubsub:
|
||||||
|
|
||||||
# TODO Consider to create a separate function called "create_atom_entry"
|
# TODO Consider to create a separate function called "create_atom_entry"
|
||||||
# or "create_rfc4287_entry" for anything related to variable "node_entry".
|
# or "create_rfc4287_entry" for anything related to variable "node_entry".
|
||||||
def create_entry(self, jid, node_id, item_id, node_item):
|
def create_entry(self, jid_bare, node_id, item_id, node_item):
|
||||||
iq = self.Iq(stype="set", sto=jid)
|
iq = self.Iq(stype="set", sto=jid_bare)
|
||||||
iq['pubsub']['publish']['node'] = node_id
|
iq['pubsub']['publish']['node'] = node_id
|
||||||
|
|
||||||
item = pubsub.Item()
|
item = pubsub.Item()
|
||||||
|
@ -153,8 +152,8 @@ class XmppPubsub:
|
||||||
return iq
|
return iq
|
||||||
|
|
||||||
|
|
||||||
def _create_entry(self, jid, node, entry, version):
|
def _create_entry(self, jid_bare, node, entry, version):
|
||||||
iq = self.Iq(stype="set", sto=jid)
|
iq = self.Iq(stype="set", sto=jid_bare)
|
||||||
iq['pubsub']['publish']['node'] = node
|
iq['pubsub']['publish']['node'] = node
|
||||||
|
|
||||||
item = pubsub.Item()
|
item = pubsub.Item()
|
||||||
|
@ -294,13 +293,15 @@ class XmppPubsubAction:
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
|
||||||
async def send_unread_items(self, jid_bare):
|
async def send_unread_items(self, jid_bare, publish_type):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
jid_bare : TYPE
|
jid_bare : str
|
||||||
Bare Jabber ID.
|
Bare Jabber ID.
|
||||||
|
publish_type : str
|
||||||
|
To which type of PubSub ('pep' or 'pubsub').
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -324,37 +325,55 @@ class XmppPubsubAction:
|
||||||
# Publish to node 'urn:xmpp:microblog:0' for own JID
|
# Publish to node 'urn:xmpp:microblog:0' for own JID
|
||||||
# Publish to node based on feed identifier for PubSub service.
|
# Publish to node based on feed identifier for PubSub service.
|
||||||
|
|
||||||
if jid_bare == self.boundjid.bare:
|
match publish_type:
|
||||||
node_id = 'urn:xmpp:microblog:0'
|
# XEP-0163: Personal Eventing Protocol
|
||||||
node_subtitle = None
|
# 2.2 One Publisher Per Node¶
|
||||||
node_title = None
|
# The owner-publisher for every node is the bare JID of the account owner.
|
||||||
else:
|
case 'pep':
|
||||||
# node_id = feed_properties[2]
|
node_id = 'urn:xmpp:microblog:0'
|
||||||
# node_title = feed_properties[3]
|
node_subtitle = None
|
||||||
# node_subtitle = feed_properties[5]
|
node_title = None
|
||||||
node_id = sqlite.get_feed_identifier(db_file, feed_id)
|
case 'pubsub':
|
||||||
node_id = node_id[0]
|
# node_id = feed_properties[2]
|
||||||
if not node_id:
|
# node_title = feed_properties[3]
|
||||||
counter = 0
|
# node_subtitle = feed_properties[5]
|
||||||
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 = sqlite.get_feed_identifier(db_file, feed_id)
|
||||||
node_id = node_id[0]
|
node_id = node_id[0]
|
||||||
node_title = sqlite.get_feed_title(db_file, feed_id)
|
if not node_id:
|
||||||
node_title = node_title[0]
|
counter = 0
|
||||||
node_subtitle = sqlite.get_feed_subtitle(db_file, feed_id)
|
while True:
|
||||||
node_subtitle = node_subtitle[0]
|
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]
|
||||||
|
print ([jid_bare, publish_type, node_id])
|
||||||
xep = None
|
xep = None
|
||||||
node_exist = await XmppPubsub.get_node_configuration(self, jid_bare, node_id)
|
#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])
|
||||||
if not node_exist:
|
if not node_exist:
|
||||||
iq_create_node = XmppPubsub.create_node(
|
iq_create_node = XmppPubsub.create_node(
|
||||||
self, jid_bare, node_id, xep, node_title, node_subtitle)
|
self, jid_bare, node_id, xep, node_title, node_subtitle)
|
||||||
await XmppIQ.send(self, iq_create_node)
|
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
|
||||||
entries = sqlite.get_unread_entries_of_feed(db_file, feed_id)
|
entries = sqlite.get_unread_entries_of_feed(db_file, feed_id)
|
||||||
report[url] = len(entries)
|
report[url] = len(entries)
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
|
@ -362,24 +381,63 @@ class XmppPubsubAction:
|
||||||
node_entry = Feed.create_rfc4287_entry(feed_entry)
|
node_entry = Feed.create_rfc4287_entry(feed_entry)
|
||||||
entry_url = feed_entry['link']
|
entry_url = feed_entry['link']
|
||||||
item_id = Utilities.hash_url_to_md5(entry_url)
|
item_id = Utilities.hash_url_to_md5(entry_url)
|
||||||
print('PubSub node item was sent to', jid_bare, node_id)
|
print(['PubSub node item was sent to', jid_bare, node_id])
|
||||||
print(entry_url)
|
print([entry_url, item_id])
|
||||||
print(item_id)
|
|
||||||
iq_create_entry = XmppPubsub.create_entry(
|
iq_create_entry = XmppPubsub.create_entry(
|
||||||
self, jid_bare, node_id, item_id, node_entry)
|
self, jid_bare, node_id, item_id, node_entry)
|
||||||
await XmppIQ.send(self, iq_create_entry)
|
result = await XmppIQ.send(self, iq_create_entry)
|
||||||
ix = entry[0]
|
ix = entry[0]
|
||||||
await sqlite.mark_as_read(db_file, ix)
|
await sqlite.mark_as_read(db_file, ix)
|
||||||
|
print(report)
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
|
||||||
class XmppPubsubTask:
|
class XmppPubsubTask:
|
||||||
|
|
||||||
|
|
||||||
|
async def loop_task(self, jid_bare, publish_type):
|
||||||
|
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)
|
||||||
|
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, publish_type))
|
||||||
|
await asyncio.sleep(60 * 180)
|
||||||
|
|
||||||
|
|
||||||
|
def restart_task(self, 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)
|
||||||
|
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):
|
async def task_publish(self, jid_bare):
|
||||||
db_file = config.get_pathname_to_database(jid_bare)
|
db_file = config.get_pathname_to_database(jid_bare)
|
||||||
if jid_bare not in self.settings:
|
if jid_bare not in self.settings:
|
||||||
Config.add_settings_jid(self.settings, jid_bare, db_file)
|
Config.add_settings_jid(self, jid_bare, db_file)
|
||||||
while True:
|
while True:
|
||||||
await XmppPubsubAction.send_unread_items(self, jid_bare)
|
await XmppPubsubAction.send_unread_items(self, jid_bare)
|
||||||
await asyncio.sleep(60 * 180)
|
await asyncio.sleep(60 * 180)
|
||||||
|
|
|
@ -28,7 +28,7 @@ class XmppStatus:
|
||||||
logger.debug('{}: jid: {}'.format(function_name, jid_bare))
|
logger.debug('{}: jid: {}'.format(function_name, jid_bare))
|
||||||
status_text = '📜️ Slixfeed RSS News Bot'
|
status_text = '📜️ Slixfeed RSS News Bot'
|
||||||
db_file = config.get_pathname_to_database(jid_bare)
|
db_file = config.get_pathname_to_database(jid_bare)
|
||||||
enabled = Config.get_setting_value(self.settings, jid_bare, 'enabled')
|
enabled = Config.get_setting_value(self, jid_bare, 'enabled')
|
||||||
if enabled:
|
if enabled:
|
||||||
jid_task = self.pending_tasks[jid_bare] if jid_bare in self.pending_tasks else None
|
jid_task = self.pending_tasks[jid_bare] if jid_bare in self.pending_tasks else None
|
||||||
if jid_task and len(jid_task):
|
if jid_task and len(jid_task):
|
||||||
|
@ -50,7 +50,7 @@ class XmppStatus:
|
||||||
status_text = '📬️ There are {} news items'.format(str(unread))
|
status_text = '📬️ There are {} news items'.format(str(unread))
|
||||||
else:
|
else:
|
||||||
# print('status no news for ' + jid_bare)
|
# print('status no news for ' + jid_bare)
|
||||||
status_mode = 'available'
|
status_mode = 'away'
|
||||||
status_text = '📭️ No news'
|
status_text = '📭️ No news'
|
||||||
else:
|
else:
|
||||||
# print('status disabled for ' + jid_bare)
|
# print('status disabled for ' + jid_bare)
|
||||||
|
|
|
@ -6,47 +6,51 @@ Based on http_upload.py example from project slixmpp
|
||||||
https://codeberg.org/poezio/slixmpp/src/branch/master/examples/http_upload.py
|
https://codeberg.org/poezio/slixmpp/src/branch/master/examples/http_upload.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from slixfeed.log import Logger
|
from slixfeed.log import Logger
|
||||||
|
from slixmpp import JID
|
||||||
from slixmpp.exceptions import IqTimeout, IqError
|
from slixmpp.exceptions import IqTimeout, IqError
|
||||||
from slixmpp.plugins.xep_0363.http_upload import HTTPError
|
from slixmpp.plugins.xep_0363.http_upload import HTTPError
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
# import sys
|
# import sys
|
||||||
|
|
||||||
class XmppUpload:
|
class XmppUpload:
|
||||||
|
|
||||||
async def start(self, jid, filename, domain=None):
|
async def start(self, jid, filename: Path, size: Optional[int] = None,
|
||||||
|
encrypted: bool = False, domain: Optional[JID] = None):
|
||||||
logger.info(['Uploading file %s...', filename])
|
logger.info(['Uploading file %s...', filename])
|
||||||
try:
|
try:
|
||||||
upload_file = self['xep_0363'].upload_file
|
upload_file = self['xep_0363'].upload_file
|
||||||
# if self.encrypted and not self['xep_0454']:
|
if encrypted and not self['xep_0454']:
|
||||||
# print(
|
print(
|
||||||
# 'The xep_0454 module isn\'t available. '
|
'The xep_0454 module isn\'t available. '
|
||||||
# 'Ensure you have \'cryptography\' '
|
'Ensure you have \'cryptography\' '
|
||||||
# 'from extras_require installed.',
|
'from extras_require installed.',
|
||||||
# file=sys.stderr,
|
file=sys.stderr,
|
||||||
# )
|
|
||||||
# return
|
|
||||||
# elif self.encrypted:
|
|
||||||
# upload_file = self['xep_0454'].upload_file
|
|
||||||
try:
|
|
||||||
url = await upload_file(
|
|
||||||
filename, domain, timeout=10,
|
|
||||||
)
|
)
|
||||||
|
url = None
|
||||||
|
elif encrypted:
|
||||||
|
upload_file = self['xep_0454'].upload_file
|
||||||
|
try:
|
||||||
|
url = await upload_file(filename, size, domain, timeout=10,)
|
||||||
logger.info('Upload successful!')
|
logger.info('Upload successful!')
|
||||||
logger.info(['Sending file to %s', jid])
|
logger.info(['Sending file to %s', jid])
|
||||||
except HTTPError:
|
except HTTPError:
|
||||||
url = ('Error: It appears that this server does not support '
|
url = None
|
||||||
'HTTP File Upload.')
|
|
||||||
logger.error('It appears that this server does not support '
|
logger.error('It appears that this server does not support '
|
||||||
'HTTP File Upload.')
|
'HTTP File Upload.')
|
||||||
# raise HTTPError(
|
# raise HTTPError(
|
||||||
# "This server doesn't appear to support HTTP File Upload"
|
# "This server doesn't appear to support HTTP File Upload"
|
||||||
# )
|
# )
|
||||||
except IqError as e:
|
except IqError as e:
|
||||||
|
url = None
|
||||||
logger.error('Could not send message')
|
logger.error('Could not send message')
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
except IqTimeout as e:
|
except IqTimeout as e:
|
||||||
|
url = None
|
||||||
# raise TimeoutError('Could not send message in time')
|
# raise TimeoutError('Could not send message in time')
|
||||||
logger.error('Could not send message in time')
|
logger.error('Could not send message in time')
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
|
|
@ -9,9 +9,13 @@ logger = Logger(__name__)
|
||||||
# class XmppChat
|
# class XmppChat
|
||||||
# class XmppUtility:
|
# class XmppUtility:
|
||||||
|
|
||||||
|
|
||||||
class XmppUtilities:
|
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):
|
async def get_chat_type(self, jid):
|
||||||
"""
|
"""
|
||||||
|
@ -60,21 +64,18 @@ class XmppUtilities:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def is_access(self, jid, chat_type):
|
||||||
def is_access(self, jid_bare, jid_full, chat_type):
|
|
||||||
"""Determine access privilege"""
|
"""Determine access privilege"""
|
||||||
operator = XmppUtilities.is_operator(self, jid_bare)
|
room = jid_bare = jid.bare
|
||||||
if operator:
|
alias = jid.resource
|
||||||
if chat_type == 'groupchat':
|
if chat_type == 'groupchat':
|
||||||
if XmppUtilities.is_moderator(self, jid_bare, jid_full):
|
access = True if XmppUtilities.is_moderator(self, room, alias) else False
|
||||||
access = True
|
if access: print('Access granted to groupchat moderator ' + alias)
|
||||||
else:
|
|
||||||
access = True
|
|
||||||
else:
|
else:
|
||||||
access = False
|
print('Access granted to chat jid ' + jid_bare)
|
||||||
|
access = True
|
||||||
return access
|
return access
|
||||||
|
|
||||||
|
|
||||||
def is_operator(self, jid_bare):
|
def is_operator(self, jid_bare):
|
||||||
"""Check if given JID is an operator"""
|
"""Check if given JID is an operator"""
|
||||||
result = False
|
result = False
|
||||||
|
@ -85,24 +86,28 @@ class XmppUtilities:
|
||||||
break
|
break
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def is_admin(self, room, alias):
|
||||||
def is_moderator(self, jid_bare, jid_full):
|
"""Check if given JID is an administrator"""
|
||||||
"""Check if given JID is a moderator"""
|
affiliation = self.plugin['xep_0045'].get_jid_property(room, alias, 'affiliation')
|
||||||
alias = jid_full[jid_full.index('/')+1:]
|
result = True if affiliation == 'admin' else False
|
||||||
role = self.plugin['xep_0045'].get_jid_property(jid_bare, alias, 'role')
|
|
||||||
if role == 'moderator':
|
|
||||||
result = True
|
|
||||||
else:
|
|
||||||
result = False
|
|
||||||
return result
|
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):
|
||||||
|
"""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
|
||||||
|
return result
|
||||||
|
|
||||||
|
# NOTE Would this properly work when Alias and Local differ?
|
||||||
def is_member(self, jid_bare, jid_full):
|
def is_member(self, jid_bare, jid_full):
|
||||||
"""Check if given JID is a member"""
|
"""Check if given JID is a member"""
|
||||||
alias = jid_full[jid_full.index('/')+1:]
|
alias = jid_full[jid_full.index('/')+1:]
|
||||||
affiliation = self.plugin['xep_0045'].get_jid_property(jid_bare, alias, 'affiliation')
|
affiliation = self.plugin['xep_0045'].get_jid_property(jid_bare, alias, 'affiliation')
|
||||||
if affiliation == 'member':
|
result = True if affiliation == 'member' else False
|
||||||
result = True
|
|
||||||
else:
|
|
||||||
result = False
|
|
||||||
return result
|
return result
|
Loading…
Reference in a new issue