forked from sch/KaikOut
440 lines
22 KiB
Python
440 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import asyncio
|
|
from datetime import datetime
|
|
import slixmpp
|
|
from kaikout.about import Documentation
|
|
from kaikout.database import Toml
|
|
from kaikout.log import Logger
|
|
from kaikout.utilities import Config, Log
|
|
from kaikout.xmpp.bookmark import XmppBookmark
|
|
from kaikout.xmpp.chat import XmppChat
|
|
from kaikout.xmpp.commands import XmppCommands
|
|
from kaikout.xmpp.groupchat import XmppGroupchat
|
|
from kaikout.xmpp.message import XmppMessage
|
|
from kaikout.xmpp.moderation import XmppModeration
|
|
from kaikout.xmpp.muc import XmppMuc
|
|
from kaikout.xmpp.status import XmppStatus
|
|
import time
|
|
|
|
# time_now = datetime.now()
|
|
# time_now = time_now.strftime("%H:%M:%S")
|
|
|
|
# def print_time():
|
|
# # return datetime.now().strftime("%H:%M:%S")
|
|
# now = datetime.now()
|
|
# current_time = now.strftime("%H:%M:%S")
|
|
# return current_time
|
|
|
|
logger = Logger(__name__)
|
|
|
|
class XmppClient(slixmpp.ClientXMPP):
|
|
|
|
"""
|
|
KaikOut - A moderation chat bot for Jabber/XMPP.
|
|
KaikOut is a chat control bot for XMPP groupchats.
|
|
"""
|
|
|
|
def __init__(self, jid, password, hostname, port, alias):
|
|
slixmpp.ClientXMPP.__init__(self, jid, password, hostname, port, alias)
|
|
# Handlers for action messages.
|
|
self.actions = {}
|
|
self.action_count = 0
|
|
# A handler for alias.
|
|
self.alias = alias
|
|
# A handler for configuration.
|
|
self.defaults = Config.get_values('settings.toml', 'defaults')
|
|
# Handlers for connectivity.
|
|
self.connection_attempts = 0
|
|
self.max_connection_attempts = 10
|
|
self.task_ping_instance = {}
|
|
self.reconnect_timeout = Config.get_values('accounts.toml', 'xmpp')['settings']['reconnect_timeout']
|
|
# A handler for operators.
|
|
self.operators = Config.get_values('accounts.toml', 'xmpp')['operators']
|
|
# A handler for settings.
|
|
self.settings = {}
|
|
# A handler for sessions.
|
|
self.sessions = {}
|
|
# A handler for tasks.
|
|
self.tasks = {}
|
|
# Register plugins.
|
|
self.register_plugin('xep_0030') # Service Discovery
|
|
self.register_plugin('xep_0004') # Data Forms
|
|
self.register_plugin('xep_0045') # Multi-User Chat
|
|
self.register_plugin('xep_0048') # Bookmarks
|
|
self.register_plugin('xep_0060') # Publish-Subscribe
|
|
self.register_plugin('xep_0050') # Ad-Hoc Commands
|
|
self.register_plugin('xep_0084') # User Avatar
|
|
self.register_plugin('xep_0085') # Chat State Notifications
|
|
self.register_plugin('xep_0115') # Entity Capabilities
|
|
self.register_plugin('xep_0122') # Data Forms Validation
|
|
self.register_plugin('xep_0199') # XMPP Ping
|
|
self.register_plugin('xep_0249') # Direct MUC Invitations
|
|
self.register_plugin('xep_0369') # Mediated Information eXchange (MIX)
|
|
self.register_plugin('xep_0437') # Room Activity Indicators
|
|
self.register_plugin('xep_0444') # Message Reactions
|
|
# Register events.
|
|
# self.add_event_handler("chatstate_composing", self.on_chatstate_composing)
|
|
# self.add_event_handler('connection_failed', self.on_connection_failed)
|
|
self.add_event_handler("disco_info", self.on_disco_info)
|
|
self.add_event_handler("groupchat_direct_invite", self.on_groupchat_direct_invite) # XEP_0249
|
|
self.add_event_handler("groupchat_invite", self.on_groupchat_invite) # XEP_0045
|
|
self.add_event_handler("message", self.on_message)
|
|
# self.add_event_handler("reactions", self.on_reactions)
|
|
# self.add_event_handler("room_activity", self.on_room_activity)
|
|
# self.add_event_handler("session_resumed", self.on_session_resumed)
|
|
self.add_event_handler("session_start", self.on_session_start)
|
|
# Connect and process.
|
|
self.connect()
|
|
self.process()
|
|
|
|
def muc_online(self, presence):
|
|
"""
|
|
Process a presence stanza from a chat room. In this case,
|
|
presences from users that have just come online are
|
|
handled by sending a welcome message that includes
|
|
the user's nickname and role in the room.
|
|
|
|
Arguments:
|
|
presence -- The received presence stanza. See the
|
|
documentation for the Presence stanza
|
|
to see how else it may be used.
|
|
"""
|
|
if presence['muc']['nick'] != self.alias:
|
|
self.send_message(mto=presence['from'].bare,
|
|
mbody="Hello, %s %s" % (presence['muc']['role'],
|
|
presence['muc']['nick']),
|
|
mtype='groupchat')
|
|
|
|
|
|
async def on_disco_info(self, DiscoInfo):
|
|
jid = DiscoInfo['from']
|
|
await self['xep_0115'].update_caps(jid=jid)
|
|
# jid_bare = DiscoInfo['from'].bare
|
|
|
|
|
|
# TODO Test
|
|
async def on_groupchat_invite(self, message):
|
|
jid_full = str(message['from'])
|
|
room = message['groupchat_invite']['jid']
|
|
result = await XmppMuc.join(self, room)
|
|
if result == 'ban':
|
|
message_body = '{} is banned from {}'.format(self.alias, room)
|
|
jid_bare = message['from'].bare
|
|
# This might not be necessary because JID might not be of the inviter, but rather of the MUC
|
|
XmppMessage.send(self, jid_bare, message_body, 'chat')
|
|
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')
|
|
XmppStatus.send_status_message(self, room)
|
|
|
|
|
|
async def on_groupchat_direct_invite(self, message):
|
|
room = message['groupchat_invite']['jid']
|
|
result = await XmppMuc.join(self, room)
|
|
if result == 'ban':
|
|
message_body = '{} is banned from {}'.format(self.alias, room)
|
|
jid_bare = message['from'].bare
|
|
XmppMessage.send(self, jid_bare, message_body, 'chat')
|
|
logger.warning(message_body)
|
|
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')
|
|
XmppStatus.send_status_message(self, room)
|
|
|
|
|
|
async def on_message(self, message):
|
|
await XmppChat.process_message(self, message)
|
|
# if message['type'] == 'groupchat':
|
|
# if 'mucroom' in message.keys():
|
|
if message['mucroom']:
|
|
alias = message['mucnick']
|
|
message_body = message['body']
|
|
identifier = message['id']
|
|
lang = message['lang']
|
|
room = message['mucroom']
|
|
timestamp_iso = datetime.now().isoformat()
|
|
fields = ['message', timestamp_iso, alias, message_body, lang, identifier]
|
|
filename = datetime.today().strftime('%Y-%m-%d') + '_' + room
|
|
Log.csv(filename, fields)
|
|
db_file = Toml.instantiate(self, room)
|
|
timestamp = time.time()
|
|
jid_bare = XmppMuc.get_full_jid(self, room, alias).split('/')[0]
|
|
XmppCommands.update_last_activity(self, room, jid_bare, db_file, timestamp)
|
|
# Toml.load_jid_settings(self, room)
|
|
# await XmppChat.process_message(self, message)
|
|
if (XmppMuc.is_moderator(self, room, self.alias) and
|
|
self.settings[room]['enabled'] and
|
|
alias != self.alias):
|
|
identifier = message['id']
|
|
fields = [alias, message_body, identifier, timestamp]
|
|
Log.toml(self, room, fields, 'message')
|
|
# Check for message
|
|
if self.settings[room]['check_message']:
|
|
reason = XmppModeration.moderate_message(self, message_body, room)
|
|
if reason:
|
|
score_max = self.settings[room]['score_messages']
|
|
score = XmppCommands.raise_score(self, room, alias, db_file, reason)
|
|
if score > score_max:
|
|
if self.settings[room]['action']:
|
|
jid_bare = await XmppCommands.outcast(self, room, alias, reason)
|
|
# admins = await XmppMuc.get_affiliation(self, room, 'admin')
|
|
# owners = await XmppMuc.get_affiliation(self, room, 'owner')
|
|
moderators = await XmppMuc.get_role(self, room, 'moderator')
|
|
# Report to the moderators.
|
|
message_to_moderators = (
|
|
'Participant {} ({}) has been banned from '
|
|
'groupchat {}.'.format(alias, jid_bare, room))
|
|
for alias in moderators:
|
|
jid_full = XmppMuc.get_full_jid(self, room, alias)
|
|
XmppMessage.send(self, jid_full, message_to_moderators, 'chat')
|
|
# Inform the subject
|
|
message_to_participant = (
|
|
'You were banned from groupchat {}. Please '
|
|
'contact the moderators if you think this was '
|
|
'a mistake.'.format(room))
|
|
XmppMessage.send(self, jid_bare, message_to_participant, 'chat')
|
|
else:
|
|
await XmppCommands.devoice(self, room, alias, reason)
|
|
# Check for inactivity
|
|
if self.settings[room]['check_inactivity']:
|
|
roster_muc = XmppMuc.get_roster(self, room)
|
|
for alias in roster_muc:
|
|
if alias != self.alias:
|
|
jid_bare = XmppMuc.get_full_jid(self, room, alias).split('/')[0]
|
|
result, span = XmppModeration.moderate_last_activity(
|
|
self, room, jid_bare, timestamp)
|
|
if result:
|
|
message_to_participant = None
|
|
if 'inactivity_notice' not in self.settings[room]:
|
|
self.settings[room]['inactivity_notice'] = []
|
|
noticed_jids = self.settings[room]['inactivity_notice']
|
|
if result == 'Inactivity':
|
|
if jid_bare in noticed_jids: noticed_jids.remove(jid_bare)
|
|
await XmppCommands.kick(self, room, alias, reason)
|
|
message_to_participant = (
|
|
'You were expelled from groupchat {} due to '
|
|
'being inactive for {} days.'.format(room, span))
|
|
elif result == 'Warning' and jid_bare not in noticed_jids:
|
|
noticed_jids.append(jid_bare)
|
|
time_left = int(span)
|
|
if not time_left: time_left = 'an'
|
|
message_to_participant = (
|
|
'This is an inactivity-warning.\n'
|
|
'You are expected to be expelled from '
|
|
'groupchat {} within {} hour time.'
|
|
.format(room, int(span) or 'an'))
|
|
Toml.update_jid_settings(
|
|
self, room, db_file, 'inactivity_notice', noticed_jids)
|
|
if message_to_participant:
|
|
XmppMessage.send(
|
|
self, jid_bare, message_to_participant, 'chat')
|
|
|
|
|
|
async def on_muc_presence(self, presence):
|
|
alias = presence['muc']['nick']
|
|
identifier = presence['id']
|
|
jid_full = presence['muc']['jid']
|
|
jid_bare = jid_full.bare
|
|
lang = presence['lang']
|
|
presence_body = presence['status']
|
|
room = presence['muc']['room']
|
|
timestamp_iso = datetime.now().isoformat()
|
|
fields = ['presence', timestamp_iso, alias, presence_body, lang, identifier]
|
|
filename = datetime.today().strftime('%Y-%m-%d') + '_' + room
|
|
# if identifier and presence_body:
|
|
Log.csv(filename, fields)
|
|
db_file = Toml.instantiate(self, room)
|
|
if (XmppMuc.is_moderator(self, room, self.alias) and
|
|
self.settings[room]['enabled'] and
|
|
alias != self.alias):
|
|
# import time # FIXME Why is this required if it is already stated at the top?
|
|
timestamp = time.time()
|
|
fields = [alias, presence_body, identifier, timestamp]
|
|
Log.toml(self, room, fields, 'presence')
|
|
# Check for status message
|
|
if self.settings[room]['check_status']:
|
|
reason, timer = XmppModeration.moderate_status_message(self, presence_body, room)
|
|
if reason and timer and not (room in self.tasks and
|
|
jid_bare in self.tasks[room] and
|
|
'countdown' in self.tasks[room][jid_bare]):
|
|
print('reason and timer for jid: ' + jid_bare + ' at room ' + room)
|
|
score_max = self.settings[room]['score_presence']
|
|
score = XmppCommands.raise_score(self, room, alias, db_file, reason)
|
|
if room not in self.tasks:
|
|
self.tasks[room] = {}
|
|
if jid_bare not in self.tasks[room]:
|
|
self.tasks[room][jid_bare] = {}
|
|
# if 'countdown' in self.tasks[room][jid_bare]:
|
|
# self.tasks[room][jid_bare]['countdown'].cancel()
|
|
if 'countdown' not in self.tasks[room][jid_bare]:
|
|
seconds = self.settings[room]['timer']
|
|
self.tasks[room][jid_bare]['countdown'] = asyncio.create_task(
|
|
XmppCommands.countdown(self, seconds, room, alias, reason))
|
|
message_to_participant = (
|
|
'Your status message "{}" violates policies of groupchat '
|
|
'{}.\n'
|
|
'You have {} seconds to change your status message, in '
|
|
'order to avoid consequent actions.'
|
|
.format(presence_body, room, seconds))
|
|
XmppMessage.send(self, jid_bare, message_to_participant, 'chat')
|
|
elif reason and not (room in self.tasks
|
|
and jid_bare in self.tasks[room] and
|
|
'countdown' in self.tasks[room][jid_bare]):
|
|
print('reason for jid: ' + jid_bare + ' at room ' + room)
|
|
score_max = self.settings[room]['score_presence']
|
|
score = XmppCommands.raise_score(self, room, alias, db_file, reason)
|
|
if score > score_max:
|
|
if self.settings[room]['action']:
|
|
jid_bare = await XmppCommands.outcast(self, room, alias, reason)
|
|
# admins = await XmppMuc.get_affiliation(self, room, 'admin')
|
|
# owners = await XmppMuc.get_affiliation(self, room, 'owner')
|
|
moderators = await XmppMuc.get_role(self, room, 'moderator')
|
|
# Report to the moderators.
|
|
message_to_moderators = (
|
|
'Participant {} ({}) has been banned from '
|
|
'groupchat {}.'.format(alias, jid_bare, room))
|
|
for alias in moderators:
|
|
# jid_full = presence['muc']['jid']
|
|
jid_full = XmppMuc.get_full_jid(self, room, alias)
|
|
XmppMessage.send(self, jid_full, message_to_moderators, 'chat')
|
|
# Inform the subject.
|
|
message_to_participant = (
|
|
'You were banned from groupchat {}. Please '
|
|
'contact the moderators if you think this was a '
|
|
'mistake.'.format(room))
|
|
XmppMessage.send(self, jid_bare, message_to_participant, 'chat')
|
|
else:
|
|
await XmppCommands.devoice(self, room, alias, reason)
|
|
elif (room in self.tasks and
|
|
jid_bare in self.tasks[room] and
|
|
'countdown' in self.tasks[room][jid_bare]) and not reason:
|
|
print('cancel task for jid: ' + jid_bare + ' at room ' + room)
|
|
print(self.tasks[room][jid_bare]['countdown'])
|
|
if self.tasks[room][jid_bare]['countdown'].cancel():
|
|
print(self.tasks[room][jid_bare]['countdown'])
|
|
message_to_participant = 'Thank you for your cooperation.'
|
|
XmppMessage.send(self, jid_bare, message_to_participant, 'chat')
|
|
del self.tasks[room][jid_bare]['countdown']
|
|
# Check for inactivity
|
|
if self.settings[room]['check_inactivity']:
|
|
roster_muc = XmppMuc.get_roster(self, room)
|
|
for alias in roster_muc:
|
|
if alias != self.alias:
|
|
jid_bare = XmppMuc.get_full_jid(self, room, alias).split('/')[0]
|
|
result, span = XmppModeration.moderate_last_activity(
|
|
self, room, jid_bare, timestamp)
|
|
if result:
|
|
message_to_participant = None
|
|
if 'inactivity_notice' not in self.settings[room]:
|
|
self.settings[room]['inactivity_notice'] = []
|
|
noticed_jids = self.settings[room]['inactivity_notice']
|
|
if result == 'Inactivity':
|
|
if jid_bare in noticed_jids: noticed_jids.remove(jid_bare)
|
|
await XmppCommands.kick(self, room, alias, reason)
|
|
message_to_participant = (
|
|
'You were expelled from groupchat {} due to '
|
|
'being inactive for {} days.'.format(room, span))
|
|
elif result == 'Warning' and jid_bare not in noticed_jids:
|
|
noticed_jids.append(jid_bare)
|
|
time_left = int(span)
|
|
if not time_left: time_left = 'an'
|
|
message_to_participant = (
|
|
'This is an inactivity-warning.\n'
|
|
'You are expected to be expelled from '
|
|
'groupchat {} within {} hour time.'
|
|
.format(room, int(span) or 'an'))
|
|
Toml.update_jid_settings(
|
|
self, room, db_file, 'inactivity_notice', noticed_jids)
|
|
if message_to_participant:
|
|
XmppMessage.send(
|
|
self, jid_bare, message_to_participant, 'chat')
|
|
|
|
|
|
async def on_muc_self_presence(self, presence):
|
|
actor = presence['muc']['item']['actor']['nick']
|
|
alias = presence['muc']['nick']
|
|
room = presence['muc']['room']
|
|
if actor and alias == self.alias: XmppStatus.send_status_message(self, room)
|
|
|
|
|
|
async def on_room_activity(self, presence):
|
|
print('on_room_activity')
|
|
print(presence)
|
|
print('testing mix core')
|
|
breakpoint()
|
|
|
|
|
|
async def on_session_start(self, event):
|
|
"""
|
|
Process the session_start event.
|
|
|
|
Typical actions for the session_start event are
|
|
requesting the roster and broadcasting an initial
|
|
presence stanza.
|
|
|
|
Arguments:
|
|
event -- An empty dictionary. The session_start
|
|
event does not provide any additional
|
|
data.
|
|
"""
|
|
# self.command_list()
|
|
# await self.get_roster()
|
|
await self['xep_0115'].update_caps()
|
|
bookmarks = await XmppBookmark.get_bookmarks(self)
|
|
print(bookmarks)
|
|
rooms = await XmppGroupchat.autojoin(self, bookmarks)
|
|
# See also get_joined_rooms of slixmpp.plugins.xep_0045
|
|
for room in rooms:
|
|
XmppStatus.send_status_message(self, room)
|
|
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)
|
|
await asyncio.sleep(5)
|
|
self.send_presence(
|
|
pshow='available',
|
|
pstatus='👁️ KaikOut Moderation Chat Bot')
|
|
|
|
|
|
def command_list(self):
|
|
self['xep_0050'].add_command(node='search',
|
|
name='🔍️ Search',
|
|
handler=self._handle_search)
|
|
self['xep_0050'].add_command(node='settings',
|
|
name='⚙️ Settings',
|
|
handler=self._handle_settings)
|
|
self['xep_0050'].add_command(node='about',
|
|
name='📜️ About',
|
|
handler=self._handle_about)
|
|
|
|
|
|
def _handle_cancel(self, payload, session):
|
|
text_note = ('Operation has been cancelled.'
|
|
'\n\n'
|
|
'No action was taken.')
|
|
session['notes'] = [['info', text_note]]
|
|
return session
|
|
|
|
|
|
def _handle_about(self, iq, session):
|
|
text_note = Documentation.about()
|
|
session['notes'] = [['info', text_note]]
|
|
return session
|