Compare commits

..

2 commits
main ... main

19 changed files with 944 additions and 464 deletions

View file

@ -99,6 +99,12 @@ Start by executing the command `kaikout` and enter Username and Password of an e
$ kaikout $ kaikout
``` ```
You can also start KaikOut as follows:
```
$ kaikout --jid ACCOUNT_JABBER_ID --password ACCOUNT_PASSWORD
```
It is advised to use a dedicated extra account for KaikOut. It is advised to use a dedicated extra account for KaikOut.
## Recommended Clients ## Recommended Clients

View file

@ -6,37 +6,18 @@
# See the file LICENSE for copying permission. # See the file LICENSE for copying permission.
# from kaikout.about import Documentation # from kaikout.about import Documentation
from kaikout.utilities import Config, Toml from kaikout.utilities import Config
# from kaikout.xmpp.chat import XmppChat # from kaikout.xmpp.chat import XmppChat
from kaikout.xmpp.client import XmppClient from kaikout.xmpp.client import XmppClient
from getpass import getpass from getpass import getpass
from argparse import ArgumentParser from argparse import ArgumentParser
from erdhe import integrate_with_kaikout # Add this import
import logging import logging
import os # import os
import shutil
# import slixmpp # import slixmpp
# import sys # import sys
def main(): def main():
directory = os.path.dirname(__file__)
# Copy data files
directory_data = Config.get_default_config_directory()
# TODO Utilize the actual data directory
#directory_data = Config.get_default_data_directory()
if not os.path.exists(directory_data):
directory_assets = os.path.join(directory, 'assets')
directory_assets_new = shutil.copytree(directory_assets, directory_data)
print(f'Data directory {directory_assets_new} has been created and populated.')
# Copy settings files
directory_settings = Config.get_default_config_directory()
if not os.path.exists(directory_settings):
directory_configs = os.path.join(directory, 'configs')
directory_settings_new = shutil.copytree(directory_configs, directory_settings)
print(f'Configuration directory {directory_settings_new} has been created and populated.')
# Setup the command line arguments. # Setup the command line arguments.
parser = ArgumentParser(description=XmppClient.__doc__) parser = ArgumentParser(description=XmppClient.__doc__)
@ -48,52 +29,39 @@ def main():
action="store_const", dest="loglevel", action="store_const", dest="loglevel",
const=logging.DEBUG, default=logging.INFO) const=logging.DEBUG, default=logging.INFO)
# JID and password options.
parser.add_argument("-j", "--jid", dest="jid",
help="JID to use")
parser.add_argument("-p", "--password", dest="password",
help="password to use")
args = parser.parse_args() args = parser.parse_args()
# Setup logging. # Setup logging.
logging.basicConfig(level=args.loglevel, logging.basicConfig(level=args.loglevel,
format='%(levelname)-8s %(message)s') format='%(levelname)-8s %(message)s')
# Configure settings file account_xmpp = Config.get_values('accounts.toml', 'xmpp')
file_settings = os.path.join(directory_settings, 'accounts.toml')
if not os.path.exists(file_settings):
directory_configs = os.path.join(directory, 'configs')
file_settings_empty = os.path.join(directory_configs, 'accounts.toml')
shutil.copyfile(file_settings_empty, file_settings)
data_settings = Toml.open_file(file_settings) if args.jid is None and not account_xmpp['client']['jid']:
args.jid = input("Username: ")
# Configure account if args.password is None and not account_xmpp['client']['password']:
data_settings_account = data_settings['xmpp']['client'] args.password = getpass("Password: ")
settings_account = {
'alias': 'Set an Alias',
'jid': 'Set a Jabber ID',
'password': 'Input Password'
}
for key in settings_account:
data_settings_account_value = data_settings_account[key]
if not data_settings_account_value:
settings_account_message = settings_account[key]
while not data_settings_account_value:
if key == 'password':
data_settings_account_value = getpass(f'{settings_account_message}: ')
else:
data_settings_account_value = input(f'{settings_account_message}: ')
data_settings_account[key] = data_settings_account_value
Toml.save_file(file_settings, data_settings)
# Try configuration file # Try configuration file
jid = data_settings_account['jid'] if 'client' in account_xmpp:
password = data_settings_account['password'] jid = account_xmpp['client']['jid']
alias = data_settings_account['alias'] password = account_xmpp['client']['password']
# TODO alias = account_xmpp['client']['alias'] if 'alias' in account_xmpp['client'] else None
#hostname = account_xmpp_client['hostname'] if 'hostname' in account_xmpp_client else None hostname = account_xmpp['client']['hostname'] if 'hostname' in account_xmpp['client'] else None
#port = account_xmpp_client['port'] if 'port' in account_xmpp_client else None port = account_xmpp['client']['port'] if 'port' in account_xmpp['client'] else None
hostname = port = None
XmppClient(jid, password, hostname, port, alias) XmppClient(jid, password, hostname, port, alias)
# Create the XMPP client
xmpp_client = XmppClient(jid, password, hostname, port, alias)
# Integrate the welcome whisper feature
welcomer = integrate_with_kaikout(xmpp_client)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -26,8 +26,8 @@ BDAY = "20 June 2024"
#DESC = "" #DESC = ""
[xmpp.client] [xmpp.client]
alias = "" alias = "KaikOut"
jid = "" jid = "/KaikOut"
password = "" password = ""
[tox] [tox]

View file

@ -1,2 +0,0 @@
[entries."xmppbl.org"]
muc_bans_sha256 = []

View file

@ -16,16 +16,16 @@ class Config:
def get_default_data_directory(): def get_default_data_directory():
directory_home = os.environ.get('HOME') if os.environ.get('HOME'):
if directory_home: data_home = os.path.join(os.environ.get('HOME'), '.local', 'share')
data_home = os.path.join(directory_home, '.local', 'share')
return os.path.join(data_home, 'kaikout') return os.path.join(data_home, 'kaikout')
elif sys.platform == 'win32': elif sys.platform == 'win32':
data_home = os.environ.get('APPDATA') data_home = os.environ.get('APPDATA')
if data_home is None: if data_home is None:
return 'kaikout_data' return os.path.join(
os.path.dirname(__file__) + '/kaikout_data')
else: else:
return 'kaikout_data' return os.path.join(os.path.dirname(__file__) + '/kaikout_data')
def get_default_config_directory(): def get_default_config_directory():
@ -42,20 +42,19 @@ class Config:
str str
Path to configuration directory. Path to configuration directory.
""" """
# directory_config_home = xdg.BaseDirectory.xdg_config_home # config_home = xdg.BaseDirectory.xdg_config_home
directory_config_home = os.environ.get('XDG_CONFIG_HOME') config_home = os.environ.get('XDG_CONFIG_HOME')
if directory_config_home is None: if config_home is None:
directory_home = os.environ.get('HOME') if os.environ.get('HOME') is None:
if directory_home is None:
if sys.platform == 'win32': if sys.platform == 'win32':
directory_config_home = os.environ.get('APPDATA') config_home = os.environ.get('APPDATA')
if directory_config_home is None: if config_home is None:
return 'kaikout_config' return os.path.abspath('.')
else: else:
return 'kaikout_config' return os.path.abspath('.')
else: else:
directory_config_home = os.path.join(directory_home, '.config') config_home = os.path.join(os.environ.get('HOME'), '.config')
return os.path.join(directory_config_home, 'kaikout') return os.path.join(config_home, 'kaikout')
def get_values(filename, key=None): def get_values(filename, key=None):

View file

@ -1 +0,0 @@
bookmarks = []

View file

@ -3,6 +3,7 @@
from asyncio import Lock from asyncio import Lock
from kaikout.log import Logger from kaikout.log import Logger
from sqlite3 import connect, Error, IntegrityError
import os import os
import sys import sys
import time import time
@ -17,10 +18,429 @@ import tomllib
# # with start_action(action_type="search_entries()", query=query): # # with start_action(action_type="search_entries()", query=query):
# # with start_action(action_type="check_entry()", link=link): # # with start_action(action_type="check_entry()", link=link):
CURSORS = {}
# aiosqlite
DBLOCK = Lock()
logger = Logger(__name__) logger = Logger(__name__)
class DatabaseToml: class SQLite:
def create_connection(db_file):
"""
Create a database connection to the SQLite database
specified by db_file.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
conn : object
Connection object or None.
"""
time_begin = time.time()
function_name = sys._getframe().f_code.co_name
message_log = '{}'
logger.debug(message_log.format(function_name))
conn = None
try:
conn = connect(db_file)
conn.execute("PRAGMA foreign_keys = ON")
# return conn
except Error as e:
logger.warning('Error creating a connection to database {}.'.format(db_file))
logger.error(e)
time_end = time.time()
difference = time_end - time_begin
if difference > 1: logger.warning('{} (time: {})'.format(function_name,
difference))
return conn
def create_tables(db_file):
"""
Create SQLite tables.
Parameters
----------
db_file : str
Path to database file.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {}'
.format(function_name, db_file))
with SQLite.create_connection(db_file) as conn:
activity_table_sql = (
"""
CREATE TABLE IF NOT EXISTS activity (
id INTEGER NOT NULL,
stanza_id TEXT,
alias TEXT,
jid TEXT,
body TEXT,
thread TEXT,
PRIMARY KEY ("id")
);
"""
)
filters_table_sql = (
"""
CREATE TABLE IF NOT EXISTS filters (
id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY ("id")
);
"""
)
outcast_table_sql = (
"""
CREATE TABLE IF NOT EXISTS outcast (
id INTEGER NOT NULL,
alias TEXT,
jid TEXT,
reason TEXT,
PRIMARY KEY ("id")
);
"""
)
settings_table_sql = (
"""
CREATE TABLE IF NOT EXISTS settings (
id INTEGER NOT NULL,
key TEXT NOT NULL,
value INTEGER,
PRIMARY KEY ("id")
);
"""
)
cur = conn.cursor()
# cur = get_cursor(db_file)
cur.execute(activity_table_sql)
cur.execute(filters_table_sql)
cur.execute(outcast_table_sql)
cur.execute(settings_table_sql)
def get_cursor(db_file):
"""
Allocate a cursor to connection per database.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
CURSORS[db_file] : object
Cursor.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {}'
.format(function_name, db_file))
if db_file in CURSORS:
return CURSORS[db_file]
else:
with SQLite.create_connection(db_file) as conn:
cur = conn.cursor()
CURSORS[db_file] = cur
return CURSORS[db_file]
async def import_feeds(db_file, feeds):
"""
Insert a new feed into the feeds table.
Parameters
----------
db_file : str
Path to database file.
feeds : list
Set of feeds (Title and URL).
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {}'
.format(function_name, db_file))
async with DBLOCK:
with SQLite.create_connection(db_file) as conn:
cur = conn.cursor()
for feed in feeds:
logger.debug('{}: feed: {}'
.format(function_name, feed))
url = feed['url']
title = feed['title']
sql = (
"""
INSERT
INTO feeds_properties(
title, url)
VALUES(
?, ?)
"""
)
par = (title, url)
try:
cur.execute(sql, par)
except IntegrityError as e:
logger.warning("Skipping: " + str(url))
logger.error(e)
async def add_metadata(db_file):
"""
Insert a new feed into the feeds table.
Parameters
----------
db_file : str
Path to database file.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {}'
.format(function_name, db_file))
async with DBLOCK:
with SQLite.create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"""
SELECT id
FROM feeds_properties
ORDER BY id ASC
"""
)
ixs = cur.execute(sql).fetchall()
for ix in ixs:
feed_id = ix[0]
# Set feed status
sql = (
"""
INSERT
INTO feeds_state(
feed_id)
VALUES(
?)
"""
)
par = (feed_id,)
try:
cur.execute(sql, par)
except IntegrityError as e:
logger.warning(
"Skipping feed_id {} for table feeds_state".format(feed_id))
logger.error(e)
# Set feed preferences.
sql = (
"""
INSERT
INTO feeds_preferences(
feed_id)
VALUES(
?)
"""
)
par = (feed_id,)
try:
cur.execute(sql, par)
except IntegrityError as e:
logger.warning(
"Skipping feed_id {} for table feeds_preferences".format(feed_id))
logger.error(e)
async def insert_feed(db_file, url, title, identifier, entries=None, version=None,
encoding=None, language=None, status_code=None,
updated=None):
"""
Insert a new feed into the feeds table.
Parameters
----------
db_file : str
Path to database file.
url : str
URL.
title : str
Feed title.
identifier : str
Feed identifier.
entries : int, optional
Number of entries. The default is None.
version : str, optional
Type of feed. The default is None.
encoding : str, optional
Encoding of feed. The default is None.
language : str, optional
Language code of feed. The default is None.
status : str, optional
HTTP status code. The default is None.
updated : ???, optional
Date feed was last updated. The default is None.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {} url: {}'
.format(function_name, db_file, url))
async with DBLOCK:
with SQLite.create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"""
INSERT
INTO feeds_properties(
url, title, identifier, entries, version, encoding, language, updated)
VALUES(
?, ?, ?, ?, ?, ?, ?, ?)
"""
)
par = (url, title, identifier, entries, version, encoding, language, updated)
cur.execute(sql, par)
sql = (
"""
SELECT id
FROM feeds_properties
WHERE url = :url
"""
)
par = (url,)
feed_id = cur.execute(sql, par).fetchone()[0]
sql = (
"""
INSERT
INTO feeds_state(
feed_id, status_code, valid)
VALUES(
?, ?, ?)
"""
)
par = (feed_id, status_code, 1)
cur.execute(sql, par)
sql = (
"""
INSERT
INTO feeds_preferences(
feed_id)
VALUES(
?)
"""
)
par = (feed_id,)
cur.execute(sql, par)
async def remove_feed_by_url(db_file, url):
"""
Delete a feed by feed URL.
Parameters
----------
db_file : str
Path to database file.
url : str
URL of feed.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {} url: {}'
.format(function_name, db_file, url))
with SQLite.create_connection(db_file) as conn:
async with DBLOCK:
cur = conn.cursor()
sql = (
"""
DELETE
FROM feeds_properties
WHERE url = ?
"""
)
par = (url,)
cur.execute(sql, par)
async def remove_feed_by_index(db_file, ix):
"""
Delete a feed by feed ID.
Parameters
----------
db_file : str
Path to database file.
ix : str
Index of feed.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {} ix: {}'
.format(function_name, db_file, ix))
with SQLite.create_connection(db_file) as conn:
async with DBLOCK:
cur = conn.cursor()
# # NOTE Should we move DBLOCK to this line? 2022-12-23
# sql = (
# "DELETE "
# "FROM entries "
# "WHERE feed_id = ?"
# )
# par = (url,)
# cur.execute(sql, par) # Error? 2024-01-05
# sql = (
# "DELETE "
# "FROM archive "
# "WHERE feed_id = ?"
# )
# par = (url,)
# cur.execute(sql, par)
sql = (
"""
DELETE
FROM feeds_properties
WHERE id = ?
"""
)
par = (ix,)
cur.execute(sql, par)
def get_feeds_by_tag_id(db_file, tag_id):
"""
Get feeds of given tag.
Parameters
----------
db_file : str
Path to database file.
tag_id : str
Tag ID.
Returns
-------
result : tuple
List of tags.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {} tag_id: {}'
.format(function_name, db_file, tag_id))
with SQLite.create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"""
SELECT feeds_properties.*
FROM feeds_properties
INNER JOIN tagged_feeds ON feeds_properties.id = tagged_feeds.feed_id
INNER JOIN tags ON tags.id = tagged_feeds.tag_id
WHERE tags.id = ?
ORDER BY feeds_properties.title;
"""
)
par = (tag_id,)
result = cur.execute(sql, par).fetchall()
return result
class Toml:
def instantiate(self, room): def instantiate(self, room):
@ -42,16 +462,15 @@ class DatabaseToml:
object object
Coroutine object. Coroutine object.
""" """
data_dir = DatabaseToml.get_default_data_directory() data_dir = Toml.get_default_data_directory()
if not os.path.isdir(data_dir): if not os.path.isdir(data_dir):
os.mkdir(data_dir) os.mkdir(data_dir)
data_dir_toml = os.path.join(data_dir, 'toml') if not os.path.isdir(data_dir + "/toml"):
if not os.path.isdir(data_dir_toml): os.mkdir(data_dir + "/toml")
os.mkdir(data_dir_toml) filename = os.path.join(data_dir, "toml", r"{}.toml".format(room))
filename = os.path.join(data_dir_toml, f'{room}.toml')
if not os.path.exists(filename): if not os.path.exists(filename):
DatabaseToml.create_settings_file(self, filename) Toml.create_settings_file(self, filename)
DatabaseToml.load_jid_settings(self, room, filename) Toml.load_jid_settings(self, room, filename)
return filename return filename
@ -62,13 +481,14 @@ class DatabaseToml:
elif sys.platform == 'win32': elif sys.platform == 'win32':
data_home = os.environ.get('APPDATA') data_home = os.environ.get('APPDATA')
if data_home is None: if data_home is None:
return 'kaikout_data' return os.path.join(
os.path.dirname(__file__) + '/kaikout_data')
else: else:
return 'kaikout_data' return os.path.join(os.path.dirname(__file__) + '/kaikout_data')
def get_data_file(data_dir, room): def get_data_file(data_dir, room):
toml_file = os.path.join(data_dir, 'toml', f'{room}.toml') toml_file = os.path.join(data_dir, "toml", r"{}.toml".format(room))
return toml_file return toml_file
@ -79,8 +499,8 @@ class DatabaseToml:
def load_jid_settings(self, room, filename): def load_jid_settings(self, room, filename):
# data_dir = DatabaseToml.get_default_data_directory() # data_dir = Toml.get_default_data_directory()
# filename = DatabaseToml.get_data_file(data_dir, room) # filename = Toml.get_data_file(data_dir, room)
with open(filename, 'rb') as f: self.settings[room] = tomllib.load(f) with open(filename, 'rb') as f: self.settings[room] = tomllib.load(f)

55
kaikout/erdhe.py Normal file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env python3
from slixmpp import ClientXMPP
from slixmpp.exceptions import IqError, IqTimeout
import logging
class WelcomeWhisperer:
"""
A feature class that sends welcome whispers to users joining an XMPP room.
"""
def __init__(self, xmpp_client):
self.xmpp = xmpp_client
self.rooms = {}
# Register event handlers
self.xmpp.add_event_handler("groupchat_presence", self.handle_groupchat_presence)
def handle_groupchat_presence(self, presence):
"""
Handle presence stanzas from chat rooms.
"""
if presence['type'] == 'available':
room = presence['from'].bare
nick = presence['from'].resource
# Check if this is a new user joining (not already in our room roster)
if room in self.rooms and nick not in self.rooms[room]:
self.send_welcome_whisper(room, nick)
# Update our room roster
if room not in self.rooms:
self.rooms[room] = set()
self.rooms[room].add(nick)
def send_welcome_whisper(self, room, nick):
"""
Send a welcome whisper to a user who just joined the room.
"""
message = f"Welcome to the room {nick}, have a good time in the channel!"
try:
self.xmpp.send_message(
mto=room,
mbody=f"/w {nick} {message}",
mtype='groupchat'
)
logging.info(f"Sent welcome whisper to {nick} in {room}")
except Exception as e:
logging.error(f"Failed to send welcome whisper: {e}")
def integrate_with_kaikout(xmpp_client):
"""
Integrate the WelcomeWhisperer feature with an existing Kaikout XmppClient instance.
"""
welcomer = WelcomeWhisperer(xmpp_client)
return welcomer

View file

@ -4,7 +4,7 @@
import csv import csv
from email.utils import parseaddr from email.utils import parseaddr
import hashlib import hashlib
from kaikout.database import DatabaseToml from kaikout.database import Toml
from kaikout.log import Logger from kaikout.log import Logger
#import kaikout.sqlite as sqlite #import kaikout.sqlite as sqlite
import os import os
@ -37,16 +37,16 @@ class Config:
str str
Path to data directory. Path to data directory.
""" """
directory_home = os.environ.get('HOME') if os.environ.get('HOME'):
if directory_home: data_home = os.path.join(os.environ.get('HOME'), '.local', 'share')
data_home = os.path.join(directory_home, '.local', 'share')
return os.path.join(data_home, 'kaikout') return os.path.join(data_home, 'kaikout')
elif sys.platform == 'win32': elif sys.platform == 'win32':
data_home = os.environ.get('APPDATA') data_home = os.environ.get('APPDATA')
if data_home is None: if data_home is None:
return 'kaikout_data' return os.path.join(
os.path.dirname(__file__) + '/kaikout_data')
else: else:
return 'kaikout_data' return os.path.join(os.path.dirname(__file__) + '/kaikout_data')
def get_default_config_directory(): def get_default_config_directory():
@ -63,20 +63,40 @@ class Config:
str str
Path to configuration directory. Path to configuration directory.
""" """
# directory_config_home = xdg.BaseDirectory.xdg_config_home # config_home = xdg.BaseDirectory.xdg_config_home
directory_config_home = os.environ.get('XDG_CONFIG_HOME') config_home = os.environ.get('XDG_CONFIG_HOME')
if directory_config_home is None: if config_home is None:
directory_home = os.environ.get('HOME') if os.environ.get('HOME') is None:
if directory_home is None:
if sys.platform == 'win32': if sys.platform == 'win32':
directory_config_home = os.environ.get('APPDATA') config_home = os.environ.get('APPDATA')
if directory_config_home is None: if config_home is None:
return 'kaikout_config' return os.path.abspath('.')
else: else:
return 'kaikout_config' return os.path.abspath('.')
else: else:
directory_config_home = os.path.join(directory_home, '.config') config_home = os.path.join(
return os.path.join(directory_config_home, 'kaikout') 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/kaikout/'
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 f: result = tomllib.load(f)
values = result[key] if key else result
return values
class Documentation: class Documentation:
@ -116,103 +136,14 @@ class Documentation:
class Log: class Log:
def jid_exist(filename, fields):
"""
Ceck whether Alias and Jabber ID exist.
Parameters
----------
filename : str
Filename.
fields : list
jid, alias, timestamp.
Returns
-------
None.
"""
data_dir = Config.get_default_data_directory()
if not os.path.isdir(data_dir): return False
data_dir_logs = os.path.join(data_dir, 'logs')
if not os.path.isdir(data_dir_logs): return False
csv_file = os.path.join(data_dir_logs, f'{filename}.csv')
if not os.path.exists(csv_file): return False
with open(csv_file, 'r') as f:
reader = csv.reader(f)
for line in reader:
if line[0] == fields[0]:
return True
def alias_jid_exist(filename, fields):
"""
Ceck whether Alias and Jabber ID exist.
Parameters
----------
filename : str
Filename.
fields : list
jid, alias, timestamp.
Returns
-------
None.
"""
data_dir = Config.get_default_data_directory()
if not os.path.isdir(data_dir): return False
data_dir_logs = os.path.join(data_dir, 'logs')
if not os.path.isdir(data_dir_logs): return False
csv_file = os.path.join(data_dir_logs, f'{filename}.csv')
if not os.path.exists(csv_file): return False
with open(csv_file, 'r') as f:
reader = csv.reader(f)
for line in reader:
if line[0] == fields[0] and line[1] == fields[1]:
return True
def csv_jid(filename, fields):
"""
Log Jabber ID to CSV file.
Parameters
----------
filename : str
Filename.
fields : list
jid, alias, timestamp.
Returns
-------
None.
"""
data_dir = Config.get_default_data_directory()
if not os.path.isdir(data_dir): os.mkdir(data_dir)
data_dir_logs = os.path.join(data_dir, 'logs')
if not os.path.isdir(data_dir_logs): os.mkdir(data_dir_logs)
csv_file = os.path.join(data_dir_logs, f'{filename}.csv')
if not os.path.exists(csv_file):
columns = ['jid', 'alias', 'timestamp']
with open(csv_file, 'w') as f:
writer = csv.writer(f)
writer.writerow(columns)
with open(csv_file, 'a') as f:
writer = csv.writer(f)
writer.writerow(fields)
def csv(filename, fields): def csv(filename, fields):
""" """
Log message or presence to CSV file. Log message to CSV file.
Parameters Parameters
---------- ----------
filename : str message : slixmpp.stanza.message.Message
Filename. Message object.
fields : list
type, timestamp, alias, body, lang, and identifier.
Returns Returns
------- -------
@ -220,12 +151,11 @@ class Log:
""" """
data_dir = Config.get_default_data_directory() data_dir = Config.get_default_data_directory()
if not os.path.isdir(data_dir): os.mkdir(data_dir) if not os.path.isdir(data_dir): os.mkdir(data_dir)
data_dir_logs = os.path.join(data_dir, 'logs') if not os.path.isdir(data_dir + "/logs"): os.mkdir(data_dir + "/logs")
if not os.path.isdir(data_dir_logs): os.mkdir(data_dir_logs) csv_file = os.path.join(data_dir, "logs", r"{}.csv".format(filename))
csv_file = os.path.join(data_dir_logs, f'{filename}.csv')
if not os.path.exists(csv_file): if not os.path.exists(csv_file):
columns = ['type', 'timestamp', 'alias', 'body', 'lang', 'identifier'] columns = ['type', 'timestamp', 'alias', 'body', 'lang', 'identifier']
with open(csv_file, 'w') as f: with open(csv_file, 'a') as f:
writer = csv.writer(f) writer = csv.writer(f)
writer.writerow(columns) writer.writerow(columns)
with open(csv_file, 'a') as f: with open(csv_file, 'a') as f:
@ -251,8 +181,8 @@ class Log:
None. None.
""" """
alias, content, identifier, timestamp = fields alias, content, identifier, timestamp = fields
data_dir = DatabaseToml.get_default_data_directory() data_dir = Toml.get_default_data_directory()
filename = DatabaseToml.get_data_file(data_dir, room) filename = Toml.get_data_file(data_dir, room)
# filename = room + '.toml' # filename = room + '.toml'
entry = {} entry = {}
entry['alias'] = alias entry['alias'] = alias
@ -295,41 +225,44 @@ class BlockList:
with open(filename, 'w') as f: f.write(content) with open(filename, 'w') as f: f.write(content)
return filename return filename
class String:
def md5_hash(url): def load_blocklist(self):
filename = BlockList.get_filename()
with open(filename, 'rb') as f:
self.blocklist = tomllib.load(f)
def add_entry_to_blocklist(self, jabber_id, node_id, item_id):
""" """
Hash URL string to MD5 checksum. Update blocklist file.
Parameters Parameters
---------- ----------
url : str jabber_id : str
URL. Jabber ID.
node_id : str
Node name.
item_id : str
Item ID.
Returns Returns
------- -------
url_digest : str None.
Hashed URL as an MD5 checksum.
""" """
url_encoded = url.encode() if jabber_id not in self.blocklist['entries']:
url_hashed = hashlib.md5(url_encoded) self.blocklist['entries'][jabber_id] = {}
url_digest = url_hashed.hexdigest() if node_id not in self.blocklist['entries'][jabber_id]:
return url_digest self.blocklist['entries'][jabber_id][node_id] = []
self.blocklist['entries'][jabber_id][node_id].append(item_id)
data = self.blocklist
content = tomli_w.dumps(data)
filename = BlockList.get_filename()
with open(filename, 'w') as f: f.write(content)
class Toml:
def open_file(filename: str) -> dict:
with open(filename, mode="rb") as fn:
data = tomllib.load(fn)
return data
def save_file(filename: str, data: dict) -> None:
with open(filename, 'w') as fn:
data_as_string = tomli_w.dumps(data)
fn.write(data_as_string)
class Url: class Url:
def check_xmpp_uri(uri): def check_xmpp_uri(uri):
""" """
Check validity of XMPP URI. Check validity of XMPP URI.
@ -348,3 +281,26 @@ class Url:
if parseaddr(jid)[1] != jid: if parseaddr(jid)[1] != jid:
jid = False jid = False
return jid return jid
class String:
def md5_hash(url):
"""
Hash URL string to MD5 checksum.
Parameters
----------
url : str
URL.
Returns
-------
url_digest : str
Hashed URL as an MD5 checksum.
"""
url_encoded = url.encode()
url_hashed = hashlib.md5(url_encoded)
url_digest = url_hashed.hexdigest()
return url_digest

View file

@ -1,2 +1,2 @@
__version__ = '0.0.8' __version__ = '0.0.6'
__version_info__ = (0, 0, 8) __version_info__ = (0, 0, 6)

99
kaikout/xmpp/bookmark.py Normal file
View file

@ -0,0 +1,99 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TODO
1) Save groupchat name instead of jid in field name.
"""
from slixmpp.plugins.xep_0048.stanza import Bookmarks
class XmppBookmark:
async def get_bookmarks(self):
result = await self.plugin['xep_0048'].get_bookmarks()
conferences = result['private']['bookmarks']['conferences']
return conferences
async def get_bookmark_properties(self, jid):
result = await self.plugin['xep_0048'].get_bookmarks()
groupchats = result['private']['bookmarks']['conferences']
for groupchat in groupchats:
if jid == groupchat['jid']:
properties = {'password': groupchat['password'],
'jid': groupchat['jid'],
'name': groupchat['name'],
'nick': groupchat['nick'],
'autojoin': groupchat['autojoin'],
'lang': groupchat['lang']}
break
return properties
async def add(self, jid=None, properties=None):
result = await self.plugin['xep_0048'].get_bookmarks()
conferences = result['private']['bookmarks']['conferences']
groupchats = []
if properties:
properties['jid'] = properties['room'] + '@' + properties['host']
if not properties['alias']: properties['alias'] = self.alias
else:
properties = {
'jid' : jid,
'alias' : self.alias,
'name' : jid.split('@')[0],
'autojoin' : True,
'password' : None,
}
for conference in conferences:
if conference['jid'] != properties['jid']:
groupchats.extend([conference])
# FIXME Ad-hoc bookmark form is stuck
# if jid not in groupchats:
if properties['jid'] not in groupchats:
bookmarks = Bookmarks()
for groupchat in groupchats:
# if groupchat['jid'] == groupchat['name']:
# groupchat['name'] = groupchat['name'].split('@')[0]
bookmarks.add_conference(groupchat['jid'],
groupchat['nick'],
name=groupchat['name'],
autojoin=groupchat['autojoin'],
password=groupchat['password'])
bookmarks.add_conference(properties['jid'],
properties['alias'],
name=properties['name'],
autojoin=properties['autojoin'],
password=properties['password'])
# await self.plugin['xep_0048'].set_bookmarks(bookmarks)
self.plugin['xep_0048'].set_bookmarks(bookmarks)
# bookmarks = Bookmarks()
# await self.plugin['xep_0048'].set_bookmarks(bookmarks)
# print(await self.plugin['xep_0048'].get_bookmarks())
# bm = BookmarkStorage()
# bm.conferences.append(Conference(muc_jid, autojoin=True, nick=self.alias))
# await self['xep_0402'].publish(bm)
async def remove(self, jid):
result = await self.plugin['xep_0048'].get_bookmarks()
conferences = result['private']['bookmarks']['conferences']
groupchats = []
for conference in conferences:
if not conference['jid'] == jid:
groupchats.extend([conference])
bookmarks = Bookmarks()
for groupchat in groupchats:
bookmarks.add_conference(groupchat['jid'],
groupchat['nick'],
name=groupchat['name'],
autojoin=groupchat['autojoin'],
password=groupchat['password'])
await self.plugin['xep_0048'].set_bookmarks(bookmarks)

View file

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# from slixmpp import JID # from slixmpp import JID
from kaikout.database import DatabaseToml from kaikout.database import Toml
from kaikout.log import Logger from kaikout.log import Logger
from kaikout.utilities import Documentation from kaikout.utilities import Documentation
from kaikout.xmpp.commands import XmppCommands from kaikout.xmpp.commands import XmppCommands
@ -52,19 +52,13 @@ class XmppChat:
if message_type == 'groupchat': if message_type == 'groupchat':
alias = message['mucnick'] alias = message['mucnick']
room = message['mucroom'] room = message['mucroom']
self_alias = XmppUtilities.get_self_alias(self, jid_bare) alias_of_kaikout = XmppUtilities.get_self_alias(self, jid_bare)
if (message['mucnick'] == self.alias or if (message['mucnick'] == self.alias or
not XmppUtilities.is_moderator(self, room, alias) or not XmppUtilities.is_moderator(self, room, alias) or
(not message_body.startswith(self_alias + ' ') and not message_body.startswith(alias_of_kaikout)):
not message_body.startswith(self_alias + ',') and
not message_body.startswith(self_alias + ':'))):
return return
alias_of_kaikout_length = len(alias_of_kaikout) + 1
# Adding one to the length because of command = message_body[alias_of_kaikout_length:].lstrip()
# assumption that a comma or a dot is added
self_alias_length = len(self_alias) + 1
command = message_body[self_alias_length:].lstrip()
elif message_type in ('chat', 'normal'): elif message_type in ('chat', 'normal'):
command = message_body command = message_body
jid_full = jid.full jid_full = jid.full
@ -183,7 +177,7 @@ class XmppChat:
print(message) print(message)
return return
db_file = DatabaseToml.instantiate(self, room) db_file = Toml.instantiate(self, room)
# # 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

View file

@ -4,9 +4,10 @@
import asyncio import asyncio
from datetime import datetime from datetime import datetime
from kaikout.about import Documentation from kaikout.about import Documentation
from kaikout.database import DatabaseToml from kaikout.database import Toml
from kaikout.log import Logger from kaikout.log import Logger
from kaikout.utilities import Config, Log, BlockList, Toml from kaikout.utilities import Config, Log, BlockList
from kaikout.xmpp.bookmark import XmppBookmark
from kaikout.xmpp.chat import XmppChat from kaikout.xmpp.chat import XmppChat
from kaikout.xmpp.commands import XmppCommands from kaikout.xmpp.commands import XmppCommands
from kaikout.xmpp.groupchat import XmppGroupchat from kaikout.xmpp.groupchat import XmppGroupchat
@ -15,7 +16,6 @@ from kaikout.xmpp.muc import XmppMuc
from kaikout.xmpp.observation import XmppObservation from kaikout.xmpp.observation import XmppObservation
from kaikout.xmpp.pubsub import XmppPubsub from kaikout.xmpp.pubsub import XmppPubsub
from kaikout.xmpp.status import XmppStatus from kaikout.xmpp.status import XmppStatus
import os
import slixmpp import slixmpp
import time import time
@ -39,39 +39,23 @@ class XmppClient(slixmpp.ClientXMPP):
def __init__(self, jid, password, hostname, port, alias): def __init__(self, jid, password, hostname, port, alias):
slixmpp.ClientXMPP.__init__(self, jid, password, hostname, port, alias) slixmpp.ClientXMPP.__init__(self, jid, password, hostname, port, alias)
self.directory_config = Config.get_default_config_directory()
self.directory_shared = Config.get_default_data_directory()
# A handler for accounts.
filename_accounts = os.path.join(self.directory_config, 'accounts.toml')
self.data_accounts = Toml.open_file(filename_accounts)
self.data_accounts_xmpp = self.data_accounts['xmpp']
# A handler for blocklist.
self.filename_blocklist = os.path.join(self.directory_shared, 'blocklist.toml')
self.blocklist = Toml.open_file(self.filename_blocklist)
# A handler for blocklist.
filename_rtbl = os.path.join(self.directory_config, 'rtbl.toml')
self.data_rtbl = Toml.open_file(filename_rtbl)
# A handler for settings.
filename_settings = os.path.join(self.directory_config, 'settings.toml')
self.data_settings = Toml.open_file(filename_settings)
# Handlers for action messages. # Handlers for action messages.
self.actions = {} self.actions = {}
self.action_count = 0 self.action_count = 0
# A handler for alias. # A handler for alias.
self.alias = alias self.alias = alias
# A handler for bookmarks.
self.filename_bookmarks = os.path.join(self.directory_config, 'bookmarks.toml')
self.data_bookmarks = Toml.open_file(self.filename_bookmarks)
self.bookmarks = self.data_bookmarks['bookmarks']
# A handler for configuration. # A handler for configuration.
self.defaults = self.data_settings['defaults'] self.defaults = Config.get_values('settings.toml', 'defaults')
# Handlers for connectivity. # Handlers for connectivity.
self.connection_attempts = 0 self.connection_attempts = 0
self.max_connection_attempts = 10 self.max_connection_attempts = 10
self.task_ping_instance = {} self.task_ping_instance = {}
self.reconnect_timeout = self.data_accounts_xmpp['settings']['reconnect_timeout'] self.reconnect_timeout = Config.get_values('accounts.toml', 'xmpp')['settings']['reconnect_timeout']
# A handler for operators. # A handler for operators.
self.operators = self.data_accounts_xmpp['operators'] self.operators = Config.get_values('accounts.toml', 'xmpp')['operators']
# A handler for blocklist.
#self.blocklist = {}
BlockList.load_blocklist(self)
# A handler for sessions. # A handler for sessions.
self.sessions = {} self.sessions = {}
# A handler for settings. # A handler for settings.
@ -138,34 +122,48 @@ class XmppClient(slixmpp.ClientXMPP):
async def on_groupchat_invite(self, message): async def on_groupchat_invite(self, message):
jid_full = str(message['from']) jid_full = str(message['from'])
room = message['groupchat_invite']['jid'] room = message['groupchat_invite']['jid']
result = await XmppGroupchat.join(self, room) result = await XmppMuc.join(self, room)
if result != 'ban': if result == 'ban':
#self.bookmarks.append({'jid' : room, 'lang' : '', 'pass' : ''}) message_body = '{} is banned from {}'.format(self.alias, room)
if room not in self.bookmarks: self.bookmarks.append(room) jid_bare = message['from'].bare
Toml.save_file(self.filename_bookmarks, self.data_bookmarks) # This might not be necessary because JID might not be of the inviter, but rather of the MUC
message_body = ('/me moderation chat bot. Jabber ID: xmpp:' XmppMessage.send(self, jid_bare, message_body, 'chat')
f'{self.boundjid.bare}?message (groupchat_invite)') logger.warning(message_body)
print("on_groupchat_invite")
print("BAN BAN BAN BAN BAN")
print("on_groupchat_invite")
print(jid_full)
print(jid_full)
print(jid_full)
print("on_groupchat_invite")
print("BAN BAN BAN BAN BAN")
print("on_groupchat_invite")
else:
await XmppBookmark.add(self, room)
message_body = (
'Greetings! I am {}, the news anchor.\n'
'My job is to bring you the latest news from sources you '
'provide me with.\n'
'You may always reach me via xmpp:{}?message'
.format(self.alias, self.boundjid.bare))
XmppMessage.send(self, room, message_body, 'groupchat') XmppMessage.send(self, room, message_body, 'groupchat')
XmppStatus.send_status_message(self, room) XmppStatus.send_status_message(self, room)
self.add_event_handler("muc::%s::got_online" % room, self.on_muc_got_online)
self.add_event_handler("muc::%s::presence" % room, self.on_muc_presence)
self.add_event_handler("muc::%s::self-presence" % room, self.on_muc_self_presence)
async def on_groupchat_direct_invite(self, message): async def on_groupchat_direct_invite(self, message):
room = message['groupchat_invite']['jid'] room = message['groupchat_invite']['jid']
result = await XmppGroupchat.join(self, room) result = await XmppMuc.join(self, room)
if result != 'ban': if result == 'ban':
#self.bookmarks.append({'jid' : room, 'lang' : '', 'pass' : ''}) message_body = '{} is banned from {}'.format(self.alias, room)
if room not in self.bookmarks: self.bookmarks.append(room) jid_bare = message['from'].bare
Toml.save_file(self.filename_bookmarks, self.data_bookmarks) XmppMessage.send(self, jid_bare, message_body, 'chat')
message_body = ('/me moderation chat bot. Jabber ID: xmpp:' logger.warning(message_body)
f'{self.boundjid.bare}?message') else:
await XmppBookmark.add(self, room)
message_body = ('/me moderation chat bot. Jabber ID: xmpp:{}?message'
.format(self.boundjid.bare))
XmppMessage.send(self, room, message_body, 'groupchat') XmppMessage.send(self, room, message_body, 'groupchat')
XmppStatus.send_status_message(self, room) XmppStatus.send_status_message(self, room)
self.add_event_handler("muc::%s::got_online" % room, self.on_muc_got_online)
self.add_event_handler("muc::%s::presence" % room, self.on_muc_presence)
self.add_event_handler("muc::%s::self-presence" % room, self.on_muc_self_presence)
async def on_message(self, message): async def on_message(self, message):
@ -182,14 +180,11 @@ class XmppClient(slixmpp.ClientXMPP):
fields = ['message', timestamp_iso, alias, message_body, lang, identifier] fields = ['message', timestamp_iso, alias, message_body, lang, identifier]
filename = datetime.today().strftime('%Y-%m-%d') + '_' + room filename = datetime.today().strftime('%Y-%m-%d') + '_' + room
Log.csv(filename, fields) Log.csv(filename, fields)
db_file = DatabaseToml.instantiate(self, room) db_file = Toml.instantiate(self, room)
timestamp = time.time() timestamp = time.time()
jid_full = XmppMuc.get_full_jid(self, room, alias) jid_bare = XmppMuc.get_full_jid(self, room, alias).split('/')[0]
if jid_full and '/' in jid_full: XmppCommands.update_last_activity(self, room, jid_bare, db_file, timestamp)
jid_bare = jid_full.split('/')[0] # Toml.load_jid_settings(self, room)
XmppCommands.update_last_activity(
self, room, jid_bare, db_file, timestamp)
# DatabaseToml.load_jid_settings(self, room)
# await XmppChat.process_message(self, message) # await XmppChat.process_message(self, message)
if (XmppMuc.is_moderator(self, room, self.alias) and if (XmppMuc.is_moderator(self, room, self.alias) and
self.settings[room]['enabled'] and self.settings[room]['enabled'] and
@ -200,11 +195,9 @@ class XmppClient(slixmpp.ClientXMPP):
fields = [alias, message_body, identifier, timestamp] fields = [alias, message_body, identifier, timestamp]
Log.toml(self, room, fields, 'message') Log.toml(self, room, fields, 'message')
# Check for message # Check for message
await XmppObservation.observe_message(self, db_file, alias, await XmppObservation.observe_message(self, db_file, alias, message_body, room)
message_body, room)
# Check for inactivity # Check for inactivity
await XmppObservation.observe_inactivity(self, db_file, await XmppObservation.observe_inactivity(self, db_file, room)
room)
async def on_muc_got_online(self, presence): async def on_muc_got_online(self, presence):
@ -218,15 +211,6 @@ class XmppClient(slixmpp.ClientXMPP):
filename = datetime.today().strftime('%Y-%m-%d') + '_' + room filename = datetime.today().strftime('%Y-%m-%d') + '_' + room
Log.csv(filename, fields) Log.csv(filename, fields)
jid_bare = presence['muc']['jid'].bare jid_bare = presence['muc']['jid'].bare
fields = [jid_bare, alias, timestamp_iso]
if jid_bare:
if not Log.jid_exist(room, fields):
message_body = (f'Welcome, {alias}! We hope that you would '
'have a good time at this group chat.')
XmppMessage.send(self, room, message_body, 'groupchat')
Log.csv_jid(room, fields)
elif not Log.alias_jid_exist(room, fields):
Log.csv_jid(room, fields)
if (XmppMuc.is_moderator(self, room, self.alias) and if (XmppMuc.is_moderator(self, room, self.alias) and
self.settings[room]['enabled'] and self.settings[room]['enabled'] and
jid_bare and jid_bare and
@ -247,9 +231,9 @@ class XmppClient(slixmpp.ClientXMPP):
status_codes = presence['muc']['status_codes'] status_codes = presence['muc']['status_codes']
actor_alias = presence['muc']['item']['actor']['nick'] actor_alias = presence['muc']['item']['actor']['nick']
if 301 in status_codes: if 301 in status_codes:
presence_body = f'User has been banned by {actor_alias}' presence_body = 'User has been banned by {}'.format(actor_alias)
elif 307 in status_codes: elif 307 in status_codes:
presence_body = f'User has been kicked by {actor_alias}' presence_body = 'User has been kicked by {}'.format(actor_alias)
else: else:
presence_body = presence['status'] presence_body = presence['status']
room = presence['muc']['room'] room = presence['muc']['room']
@ -258,7 +242,7 @@ class XmppClient(slixmpp.ClientXMPP):
filename = datetime.today().strftime('%Y-%m-%d') + '_' + room filename = datetime.today().strftime('%Y-%m-%d') + '_' + room
# if identifier and presence_body: # if identifier and presence_body:
Log.csv(filename, fields) Log.csv(filename, fields)
db_file = DatabaseToml.instantiate(self, room) db_file = Toml.instantiate(self, room)
if (XmppMuc.is_moderator(self, room, self.alias) and if (XmppMuc.is_moderator(self, room, self.alias) and
self.settings[room]['enabled'] and self.settings[room]['enabled'] and
alias != self.alias): alias != self.alias):
@ -269,8 +253,7 @@ class XmppClient(slixmpp.ClientXMPP):
await XmppObservation.observe_strikes(self, db_file, presence, room) await XmppObservation.observe_strikes(self, db_file, presence, room)
if jid_bare and jid_bare not in self.settings[room]['jid_whitelist']: if jid_bare and jid_bare not in self.settings[room]['jid_whitelist']:
# Check for status message # Check for status message
await XmppObservation.observe_status_message( await XmppObservation.observe_status_message(self, alias, db_file, jid_bare, presence_body, room)
self, alias, db_file, jid_bare, presence_body, room)
# Check for inactivity # Check for inactivity
await XmppObservation.observe_inactivity(self, db_file, room) await XmppObservation.observe_inactivity(self, db_file, room)
@ -279,16 +262,7 @@ class XmppClient(slixmpp.ClientXMPP):
actor = presence['muc']['item']['actor']['nick'] actor = presence['muc']['item']['actor']['nick']
alias = presence['muc']['nick'] alias = presence['muc']['nick']
room = presence['muc']['room'] room = presence['muc']['room']
if actor and alias == self.alias: XmppStatus.send_status_message(self, if actor and alias == self.alias: XmppStatus.send_status_message(self, room)
room)
# TODO Check whether group chat is not anonymous
if XmppMuc.is_moderator(self, room, self.alias):
timestamp_iso = datetime.now().isoformat()
for alias in XmppMuc.get_roster(self, room):
jid_bare = XmppMuc.get_full_jid(self, room, alias).split('/')[0]
fields = [jid_bare, alias, timestamp_iso]
if not Log.alias_jid_exist(room, fields): Log.csv_jid(room,
fields)
def on_reactions(self, message): def on_reactions(self, message):
@ -326,7 +300,7 @@ class XmppClient(slixmpp.ClientXMPP):
""" """
# self.command_list() # self.command_list()
# await self.get_roster() # await self.get_roster()
rtbl_sources = self.data_rtbl['sources'] rtbl_sources = Config.get_values('rtbl.toml')['sources']
for source in rtbl_sources: for source in rtbl_sources:
jabber_id = source['jabber_id'] jabber_id = source['jabber_id']
node_id = source['node_id'] node_id = source['node_id']
@ -334,23 +308,28 @@ class XmppClient(slixmpp.ClientXMPP):
if subscribe['pubsub']['subscription']['subscription'] == 'subscribed': if subscribe['pubsub']['subscription']['subscription'] == 'subscribed':
rtbl_list = await XmppPubsub.get_items(self, jabber_id, node_id) rtbl_list = await XmppPubsub.get_items(self, jabber_id, node_id)
rtbl_items = rtbl_list['pubsub']['items'] rtbl_items = rtbl_list['pubsub']['items']
muc_bans_sha256 = self.blocklist['entries']['xmppbl.org']['muc_bans_sha256']
for item in rtbl_items: for item in rtbl_items:
exist = False
item_id = item['id'] item_id = item['id']
if item_id not in muc_bans_sha256: muc_bans_sha256.append(item_id) for jid in self.blocklist['entries']:
for node in jid:
for item in node:
if item_id == item:
exist = True
break
if not exist:
# TODO Extract items item_payload.find(namespace + 'title') # TODO Extract items item_payload.find(namespace + 'title')
# NOTE (Pdb) # NOTE (Pdb)
# for i in item['payload'].iter(): i.attrib # for i in item['payload'].iter(): i.attrib
# {'reason': 'urn:xmpp:reporting:abuse'} # {'reason': 'urn:xmpp:reporting:abuse'}
self.blocklist['entries']['xmppbl.org']['muc_bans_sha256'] = muc_bans_sha256 BlockList.add_entry_to_blocklist(self, jabber_id, node_id, item_id)
Toml.save_file(self.filename_blocklist, self.blocklist)
del self.filename_blocklist
# subscribe['from'] = xmppbl.org # subscribe['from'] = xmppbl.org
# subscribe['pubsub']['subscription']['node'] = 'muc_bans_sha256' # subscribe['pubsub']['subscription']['node'] = 'muc_bans_sha256'
subscriptions = await XmppPubsub.get_node_subscriptions( subscriptions = await XmppPubsub.get_node_subscriptions(self, jabber_id, node_id)
self, jabber_id, node_id)
await self['xep_0115'].update_caps() await self['xep_0115'].update_caps()
rooms = await XmppGroupchat.autojoin(self) bookmarks = await XmppBookmark.get_bookmarks(self)
print(bookmarks)
rooms = await XmppGroupchat.autojoin(self, bookmarks)
# See also get_joined_rooms of slixmpp.plugins.xep_0045 # See also get_joined_rooms of slixmpp.plugins.xep_0045
for room in rooms: for room in rooms:
XmppStatus.send_status_message(self, room) XmppStatus.send_status_message(self, room)

View file

@ -5,9 +5,10 @@ import asyncio
import kaikout.config as config import kaikout.config as config
from kaikout.config import Config from kaikout.config import Config
from kaikout.log import Logger from kaikout.log import Logger
from kaikout.database import DatabaseToml from kaikout.database import Toml
from kaikout.utilities import Documentation, Toml, Url from kaikout.utilities import Documentation, Url
from kaikout.version import __version__ from kaikout.version import __version__
from kaikout.xmpp.bookmark import XmppBookmark
from kaikout.xmpp.muc import XmppMuc from kaikout.xmpp.muc import XmppMuc
from kaikout.xmpp.status import XmppStatus from kaikout.xmpp.status import XmppStatus
from kaikout.xmpp.utilities import XmppUtilities from kaikout.xmpp.utilities import XmppUtilities
@ -36,7 +37,7 @@ class XmppCommands:
async def clear_filter(self, room, db_file, key): async def clear_filter(self, room, db_file, key):
value = [] value = []
self.settings[room][key] = value self.settings[room][key] = value
DatabaseToml.update_jid_settings(self, room, db_file, key, value) Toml.update_jid_settings(self, room, db_file, key, value)
message = 'Filter {} has been purged.'.format(key) message = 'Filter {} has been purged.'.format(key)
return message return message
@ -80,16 +81,17 @@ class XmppCommands:
async def bookmark_add(self, muc_jid): async def bookmark_add(self, muc_jid):
if muc_jid not in self.bookmarks: self.bookmarks.append(muc_jid) await XmppBookmark.add(self, jid=muc_jid)
Toml.save_file(self.filename_bookmarks, self.data_bookmarks) message = ('Groupchat {} has been added to bookmarks.'
return f'Groupchat {muc_jid} has been added to bookmarks.' .format(muc_jid))
return message
async def bookmark_del(self, muc_jid): async def bookmark_del(self, muc_jid):
if muc_jid in self.bookmarks: self.bookmarks.remove(muc_jid) await XmppBookmark.remove(self, muc_jid)
Toml.save_file(self.filename_bookmarks, self.data_bookmarks) message = ('Groupchat {} has been removed from bookmarks.'
return f'Groupchat {muc_jid} has been removed from bookmarks.' .format(muc_jid))
return message
async def invite_jid_to_muc(self, jid_bare): async def invite_jid_to_muc(self, jid_bare):
muc_jid = 'slixfeed@chat.woodpeckersnest.space' muc_jid = 'slixfeed@chat.woodpeckersnest.space'
@ -120,13 +122,12 @@ class XmppCommands:
if muc_jid: if muc_jid:
# TODO probe JID and confirm it's a groupchat # TODO probe JID and confirm it's a groupchat
result = await XmppMuc.join(self, muc_jid) result = await XmppMuc.join(self, muc_jid)
# await XmppBookmark.add(self, jid=muc_jid)
if result == 'ban': if result == 'ban':
message = '{} is banned from {}'.format(self.alias, muc_jid) message = '{} is banned from {}'.format(self.alias, muc_jid)
if room in self.bookmarks: self.bookmarks.remove(room)
else: else:
if room not in self.bookmarks: self.bookmarks.append(room) await XmppBookmark.add(self, muc_jid)
message = 'Joined groupchat {}'.format(muc_jid) message = 'Joined groupchat {}'.format(muc_jid)
Toml.save_file(self.filename_bookmarks, self.data_bookmarks)
else: else:
message = '> {}\nGroupchat JID appears to be invalid.'.format(muc_jid) message = '> {}\nGroupchat JID appears to be invalid.'.format(muc_jid)
else: else:
@ -136,8 +137,7 @@ class XmppCommands:
async def muc_leave(self, room): async def muc_leave(self, room):
XmppMuc.leave(self, room) XmppMuc.leave(self, room)
if room in self.bookmarks: self.bookmarks.remove(room) await XmppBookmark.remove(self, room)
Toml.save_file(self.filename_bookmarks, self.data_bookmarks)
async def outcast(self, room, alias, reason): async def outcast(self, room, alias, reason):
@ -163,11 +163,14 @@ class XmppCommands:
async def print_bookmarks(self): async def print_bookmarks(self):
conferences = self.bookmarks conferences = await XmppBookmark.get_bookmarks(self)
message = '\nList of groupchats:\n\n```\n' message = '\nList of groupchats:\n\n```\n'
for conference in conferences: for conference in conferences:
message += f'{conference}\n' message += ('Name: {}\n'
message += (f'```\nTotal of {len(conferences)} groupchats.\n') 'Room: {}\n'
'\n'
.format(conference['name'], conference['jid']))
message += ('```\nTotal of {} groupchats.\n'.format(len(conferences)))
return message return message
@ -324,7 +327,7 @@ class XmppCommands:
if jid_full: if jid_full:
jid_bare = jid_full.split('/')[0] jid_bare = jid_full.split('/')[0]
scores[jid_bare] = scores[jid_bare] + 1 if jid_bare in scores else 1 scores[jid_bare] = scores[jid_bare] + 1 if jid_bare in scores else 1
DatabaseToml.update_jid_settings(self, room, db_file, 'scores', scores) Toml.update_jid_settings(self, room, db_file, 'scores', scores)
time.sleep(5) time.sleep(5)
del self.actions[room][task_number] del self.actions[room][task_number]
XmppStatus.send_status_message(self, room) XmppStatus.send_status_message(self, room)
@ -352,7 +355,7 @@ class XmppCommands:
""" """
scores = self.settings[room]['score_ban'] if 'score_ban' in self.settings[room] else {} scores = self.settings[room]['score_ban'] if 'score_ban' in self.settings[room] else {}
scores[jid_bare] = scores[jid_bare] + 1 if jid_bare in scores else 1 scores[jid_bare] = scores[jid_bare] + 1 if jid_bare in scores else 1
DatabaseToml.update_jid_settings(self, room, db_file, 'score_ban', scores) Toml.update_jid_settings(self, room, db_file, 'score_ban', scores)
# result = scores[jid_bare] # result = scores[jid_bare]
# return result # return result
@ -377,7 +380,7 @@ class XmppCommands:
""" """
scores = self.settings[room]['score_kick'] if 'score_kick' in self.settings[room] else {} scores = self.settings[room]['score_kick'] if 'score_kick' in self.settings[room] else {}
scores[jid_bare] = scores[jid_bare] + 1 if jid_bare in scores else 1 scores[jid_bare] = scores[jid_bare] + 1 if jid_bare in scores else 1
DatabaseToml.update_jid_settings(self, room, db_file, 'score_kick', scores) Toml.update_jid_settings(self, room, db_file, 'score_kick', scores)
# result = scores[jid_bare] # result = scores[jid_bare]
# return result # return result
@ -404,7 +407,7 @@ class XmppCommands:
""" """
activity = self.settings[room]['last_activity'] if 'last_activity' in self.settings[room] else {} activity = self.settings[room]['last_activity'] if 'last_activity' in self.settings[room] else {}
activity[jid_bare] = timestamp activity[jid_bare] = timestamp
DatabaseToml.update_jid_settings(self, room, db_file, 'last_activity', activity) Toml.update_jid_settings(self, room, db_file, 'last_activity', activity)
def remove_last_activity(self, room, jid_bare, db_file): def remove_last_activity(self, room, jid_bare, db_file):
@ -427,7 +430,7 @@ class XmppCommands:
""" """
activity = self.settings[room]['last_activity'] if 'last_activity' in self.settings[room] else {} activity = self.settings[room]['last_activity'] if 'last_activity' in self.settings[room] else {}
del activity[jid_bare] del activity[jid_bare]
DatabaseToml.update_jid_settings(self, room, db_file, 'last_activity', activity) Toml.update_jid_settings(self, room, db_file, 'last_activity', activity)
def raise_score_inactivity(self, room, alias, db_file): def raise_score_inactivity(self, room, alias, db_file):
@ -459,7 +462,7 @@ class XmppCommands:
if jid_full: if jid_full:
jid_bare = jid_full.split('/')[0] jid_bare = jid_full.split('/')[0]
scores_inactivity[jid_bare] = scores_inactivity[jid_bare] + 1 if jid_bare in scores_inactivity else 1 scores_inactivity[jid_bare] = scores_inactivity[jid_bare] + 1 if jid_bare in scores_inactivity else 1
DatabaseToml.update_jid_settings(self, room, db_file, 'scores_inactivity', scores_inactivity) Toml.update_jid_settings(self, room, db_file, 'scores_inactivity', scores_inactivity)
time.sleep(5) time.sleep(5)
del self.actions[room][task_number] del self.actions[room][task_number]
XmppStatus.send_status_message(self, room) XmppStatus.send_status_message(self, room)
@ -471,16 +474,16 @@ class XmppCommands:
if key: if key:
value = self.defaults[key] value = self.defaults[key]
self.settings[room][key] = value self.settings[room][key] = value
# data_dir = DatabaseToml.get_default_data_directory() # data_dir = Toml.get_default_data_directory()
# db_file = DatabaseToml.get_data_file(data_dir, jid_bare) # db_file = Toml.get_data_file(data_dir, jid_bare)
DatabaseToml.update_jid_settings(self, room, db_file, key, value) Toml.update_jid_settings(self, room, db_file, key, value)
message = ('Setting {} has been restored to default value.' message = ('Setting {} has been restored to default value.'
.format(key)) .format(key))
else: else:
self.settings = self.defaults self.settings = self.defaults
data_dir = DatabaseToml.get_default_data_directory() data_dir = Toml.get_default_data_directory()
db_file = DatabaseToml.get_data_file(data_dir, room) db_file = Toml.get_data_file(data_dir, room)
DatabaseToml.create_settings_file(self, db_file) Toml.create_settings_file(self, db_file)
message = 'Default settings have been restored.' message = 'Default settings have been restored.'
return message return message
@ -521,7 +524,7 @@ class XmppCommands:
string_trim = string.strip() string_trim = string.strip()
string_list.remove(string_trim) string_list.remove(string_trim)
processed_strings.append(string_trim) processed_strings.append(string_trim)
DatabaseToml.update_jid_settings(self, room, db_file, filter, string_list) Toml.update_jid_settings(self, room, db_file, filter, string_list)
processed_strings.sort() processed_strings.sort()
if processed_strings: if processed_strings:
message = 'Strings "{}" have been {} list "{}".'.format( message = 'Strings "{}" have been {} list "{}".'.format(
@ -548,4 +551,4 @@ class XmppCommands:
def update_setting_value(self, room, db_file, key, value): def update_setting_value(self, room, db_file, key, value):
DatabaseToml.update_jid_settings(self, room, db_file, key, value) Toml.update_jid_settings(self, room, db_file, key, value)

View file

@ -1,52 +1,58 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from kaikout.xmpp.message import XmppMessage """
TODO
1) Send message to inviter that bot has joined to groupchat.
2) If groupchat requires captcha, send the consequent message.
3) If groupchat error is received, send that error message to inviter.
FIXME
1) Save name of groupchat instead of jid as name
"""
from kaikout.xmpp.bookmark import XmppBookmark
from kaikout.xmpp.muc import XmppMuc from kaikout.xmpp.muc import XmppMuc
from kaikout.xmpp.status import XmppStatus from kaikout.xmpp.status import XmppStatus
from kaikout.utilities import Toml
from kaikout.log import Logger, Message from kaikout.log import Logger, Message
import random
logger = Logger(__name__) logger = Logger(__name__)
class XmppGroupchat: class XmppGroupchat:
async def join(self, room): async def autojoin(self, bookmarks):
result = await XmppMuc.join(self, room) mucs_join_success = []
for bookmark in bookmarks:
if bookmark["jid"] and bookmark["autojoin"]:
if not bookmark["nick"]:
bookmark["nick"] = self.alias
logger.error('Alias (i.e. Nicknname) is missing for '
'bookmark {}'.format(bookmark['name']))
alias = bookmark["nick"]
room = bookmark["jid"]
Message.printer('Joining to MUC {} ...'.format(room))
result = await XmppMuc.join(self, room, alias)
if result == 'ban': if result == 'ban':
message_body = '{} is banned from {}'.format(self.alias, room) await XmppBookmark.remove(self, room)
jid_bare = message['from'].bare logger.warning('{} is banned from {}'.format(self.alias, room))
XmppMessage.send(self, jid_bare, message_body, 'chat') logger.warning('Groupchat {} has been removed from bookmarks'
logger.warning(message_body) .format(room))
elif result == 'conflict':
while result == 'conflict':
number = str(random.randrange(1000, 5000))
print(f'Conflict. Atempting to join to {room} as {self.alias} #{number}')
result = await XmppMuc.join(self, room, f'{self.alias} #{number}')
else: else:
#self.bookmarks.append({'jid' : room, 'lang' : '', 'pass' : ''}) mucs_join_success.append(room)
if room not in self.bookmarks: self.bookmarks.append(room) logger.info('Autojoin groupchat\n'
Toml.save_file(self.filename_bookmarks, self.data_bookmarks) 'Name : {}\n'
return result 'JID : {}\n'
'Alias : {}\n'
async def autojoin(self): .format(bookmark["name"],
mucs_joined = [] bookmark["jid"],
for room in self.bookmarks: bookmark["nick"]))
alias = self.alias elif not bookmark["jid"]:
print(f'Joining to MUC {room} ...') logger.error('JID is missing for bookmark {}'
#Message.printer(f'Joining to MUC {room} ...') .format(bookmark['name']))
result = await XmppMuc.join(self, room) return mucs_join_success
if result == 'ban':
if room in self.bookmarks: self.bookmarks.remove(room)
Toml.save_file(self.filename_bookmarks, self.data_bookmarks)
logger.warning(f'{alias} is banned from {room}')
logger.warning(f'Groupchat {room} has been removed from bookmarks')
elif result == 'conflict':
while result == 'conflict':
number = str(random.randrange(1000, 5000))
print(f'Conflict. Atempting to join to {room} as {self.alias} #{number}')
result = await XmppMuc.join(self, room, f'{alias} #{number}')
else:
mucs_joined.append(room)
return mucs_joined

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from kaikout.database import DatabaseToml from kaikout.database import Toml
from kaikout.log import Logger from kaikout.log import Logger
from kaikout.xmpp.commands import XmppCommands from kaikout.xmpp.commands import XmppCommands
@ -17,8 +17,8 @@ class XmppModeration:
if jid_bare not in self.settings[room]['last_activity']: if jid_bare not in self.settings[room]['last_activity']:
# self.settings[room]['last_activity'][jid_bare] = timestamp # self.settings[room]['last_activity'][jid_bare] = timestamp
# last_activity_for_jid = self.settings[room]['last_activity'][jid_bare] # last_activity_for_jid = self.settings[room]['last_activity'][jid_bare]
db_file = DatabaseToml.instantiate(self, room) db_file = Toml.instantiate(self, room)
# DatabaseToml.update_jid_settings(self, room, db_file, 'last_activity', last_activity_for_jid) # Toml.update_jid_settings(self, room, db_file, 'last_activity', last_activity_for_jid)
XmppCommands.update_last_activity(self, room, jid_bare, db_file, timestamp) XmppCommands.update_last_activity(self, room, jid_bare, db_file, timestamp)
else: else:
jid_last_activity = self.settings[room]['last_activity'][jid_bare] jid_last_activity = self.settings[room]['last_activity'][jid_bare]

View file

@ -16,7 +16,6 @@ FIXME
1) Save name of groupchat instead of jid as name 1) Save name of groupchat instead of jid as name
""" """
from asyncio import TimeoutError
from slixmpp.exceptions import IqError, IqTimeout, PresenceError from slixmpp.exceptions import IqError, IqTimeout, PresenceError
from kaikout.log import Logger from kaikout.log import Logger
@ -93,18 +92,40 @@ class XmppMuc:
def is_moderator(self, room, alias): def is_moderator(self, room, alias):
"""Check if given JID is a moderator""" """Check if given JID is a moderator"""
role = self.plugin['xep_0045'].get_jid_property(room, alias, 'role') role = self.plugin['xep_0045'].get_jid_property(room, alias, 'role')
result = True if role == 'moderator' else False if role == 'moderator':
result = True
else:
result = False
return result return result
async def join(self, jid_bare, alias=None, password=None): async def join(self, jid, alias=None, password=None):
logger.info('Joining groupchat\nJID : {}\n'.format(jid_bare)) # token = await initdb(
#jid_from = str(self.boundjid) if self.is_component else None # muc_jid,
# sqlite.get_setting_value,
# "token"
# )
# if token != "accepted":
# token = randrange(10000, 99999)
# await initdb(
# muc_jid,
# sqlite.update_setting_value,
# ["token", token]
# )
# self.send_message(
# mto=inviter,
# mfrom=self.boundjid.bare,
# mbody=(
# "Send activation token {} to groupchat xmpp:{}?join."
# ).format(token, muc_jid)
# )
logger.info('Joining groupchat\nJID : {}\n'.format(jid))
jid_from = str(self.boundjid) if self.is_component else None
if not alias: alias = self.alias if not alias: alias = self.alias
try: try:
await self.plugin['xep_0045'].join_muc_wait(jid_bare, await self.plugin['xep_0045'].join_muc_wait(jid,
alias, alias,
#presence_options = {"pfrom" : jid_from}, presence_options = {"pfrom" : jid_from},
password=password, password=password,
maxchars=0, maxchars=0,
maxstanzas=0, maxstanzas=0,
@ -115,35 +136,22 @@ class XmppMuc:
except IqError as e: except IqError as e:
logger.error('Error XmppIQ') logger.error('Error XmppIQ')
logger.error(str(e)) logger.error(str(e))
logger.error(jid_bare) logger.error(jid)
result = 'error' result = 'error'
except IqTimeout as e: except IqTimeout as e:
logger.error('Timeout XmppIQ') logger.error('Timeout XmppIQ')
logger.error(str(e)) logger.error(str(e))
logger.error(jid_bare) logger.error(jid)
result = 'timeout'
except TimeoutError as e:
logger.error('Timeout AsyncIO')
logger.error(str(e))
logger.error(jid_bare)
result = 'timeout' result = 'timeout'
except PresenceError as e: except PresenceError as e:
logger.error('Error Presence') logger.error('Error Presence')
logger.error(str(e)) logger.error(str(e))
if (e.condition == 'forbidden' and if (e.condition == 'forbidden' and
e.presence['error']['code'] == '403'): e.presence['error']['code'] == '403'):
logger.warning('{} is banned from {}'.format(self.alias, jid_bare)) logger.warning('{} is banned from {}'.format(self.alias, jid))
result = 'ban' result = 'ban'
elif e.condition == 'conflict':
logger.warning(e.presence['error']['text'])
result = 'conflict'
else: else:
result = 'error' result = 'error'
except Exception as e:
logger.error('Unknown error')
logger.error(str(e))
logger.error(jid_bare)
result = 'unknown'
return result return result
@ -175,11 +183,6 @@ class XmppMuc:
logger.error('Could not set affiliation at room: {}'.format(room)) logger.error('Could not set affiliation at room: {}'.format(room))
logger.error(str(e)) logger.error(str(e))
logger.error(room) logger.error(room)
except Exception as e:
logger.error('Unknown error')
logger.error('Could not set affiliation at room: {}'.format(room))
logger.error(str(e))
logger.error(room)
async def set_role(self, room, alias, role, reason=None): async def set_role(self, room, alias, role, reason=None):
@ -192,8 +195,3 @@ class XmppMuc:
logger.error('Could not set role of alias: {}'.format(alias)) logger.error('Could not set role of alias: {}'.format(alias))
logger.error(str(e)) logger.error(str(e))
logger.error(room) logger.error(room)
except Exception as e:
logger.error('Unknown error')
logger.error('Could not set role of alias: {}'.format(alias))
logger.error(str(e))
logger.error(room)

View file

@ -3,7 +3,7 @@
import asyncio import asyncio
from hashlib import sha256 from hashlib import sha256
from kaikout.database import DatabaseToml from kaikout.database import Toml
from kaikout.log import Logger from kaikout.log import Logger
from kaikout.xmpp.commands import XmppCommands from kaikout.xmpp.commands import XmppCommands
from kaikout.xmpp.message import XmppMessage from kaikout.xmpp.message import XmppMessage
@ -70,7 +70,7 @@ class XmppObservation:
'You are expected to be expelled from ' 'You are expected to be expelled from '
'groupchat {} within {} hour time.' 'groupchat {} within {} hour time.'
.format(room, int(span) or 'an')) .format(room, int(span) or 'an'))
DatabaseToml.update_jid_settings( Toml.update_jid_settings(
self, room, db_file, 'inactivity_notice', noticed_jids) self, room, db_file, 'inactivity_notice', noticed_jids)
if message_to_participant: if message_to_participant:
XmppMessage.send( XmppMessage.send(

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from kaikout.database import DatabaseToml from kaikout.database import Toml
from kaikout.log import Logger from kaikout.log import Logger
from kaikout.xmpp.presence import XmppPresence from kaikout.xmpp.presence import XmppPresence
from kaikout.xmpp.utilities import XmppUtilities from kaikout.xmpp.utilities import XmppUtilities
@ -27,8 +27,8 @@ class XmppStatus:
if not status_mode and not status_text: if not status_mode and not status_text:
if XmppUtilities.is_moderator(self, room, self.alias): if XmppUtilities.is_moderator(self, room, self.alias):
if room not in self.settings: if room not in self.settings:
DatabaseToml.instantiate(self, room) Toml.instantiate(self, room)
# DatabaseToml.load_jid_settings(self, room) # Toml.load_jid_settings(self, room)
if self.settings[room]['enabled']: if self.settings[room]['enabled']:
jid_task = self.actions[room] if room in self.actions else None jid_task = self.actions[room] if room in self.actions else None
if jid_task and len(jid_task): if jid_task and len(jid_task):