808 lines
25 KiB
Python
808 lines
25 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright © 2018, 2019 Damir Jelić <poljar@termina.org.uk>
|
|
# Copyright © 2018, 2019 Denis Kasak <dkasak@termina.org.uk>
|
|
#
|
|
# Permission to use, copy, modify, and/or distribute this software for
|
|
# any purpose with or without fee is hereby granted, provided that the
|
|
# above copyright notice and this permission notice appear in all copies.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
|
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
|
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
"""weechat-matrix Configuration module.
|
|
|
|
This module contains abstractions on top of weechats configuration files and
|
|
the main script configuration class.
|
|
|
|
To add configuration options refer to MatrixConfig.
|
|
Server specific configuration options are handled in server.py
|
|
"""
|
|
|
|
from builtins import super
|
|
from collections import namedtuple
|
|
from enum import Enum, unique
|
|
|
|
import logbook
|
|
|
|
import nio
|
|
from matrix.globals import SCRIPT_NAME, SERVERS, W
|
|
from matrix.utf import utf8_decode
|
|
|
|
from . import globals as G
|
|
|
|
|
|
@unique
|
|
class RedactType(Enum):
|
|
STRIKETHROUGH = 0
|
|
NOTICE = 1
|
|
DELETE = 2
|
|
|
|
|
|
@unique
|
|
class ServerBufferType(Enum):
|
|
MERGE_CORE = 0
|
|
MERGE = 1
|
|
INDEPENDENT = 2
|
|
|
|
|
|
nio.logger_group.level = logbook.ERROR
|
|
|
|
|
|
class Option(
|
|
namedtuple(
|
|
"Option",
|
|
[
|
|
"name",
|
|
"type",
|
|
"string_values",
|
|
"min",
|
|
"max",
|
|
"value",
|
|
"description",
|
|
"cast_func",
|
|
"change_callback",
|
|
],
|
|
)
|
|
):
|
|
"""A class representing a new configuration option.
|
|
|
|
An option object is consumed by the ConfigSection class adding
|
|
configuration options to weechat.
|
|
"""
|
|
|
|
__slots__ = ()
|
|
|
|
def __new__(
|
|
cls,
|
|
name,
|
|
type,
|
|
string_values,
|
|
min,
|
|
max,
|
|
value,
|
|
description,
|
|
cast=None,
|
|
change_callback=None,
|
|
):
|
|
"""
|
|
Parameters:
|
|
name (str): Name of the configuration option
|
|
type (str): Type of the configuration option, can be one of the
|
|
supported weechat types: string, boolean, integer, color
|
|
string_values: (str): A list of string values that the option can
|
|
accept seprated by |
|
|
min (int): Minimal value of the option, only used if the type of
|
|
the option is integer
|
|
max (int): Maximal value of the option, only used if the type of
|
|
the option is integer
|
|
description (str): Description of the configuration option
|
|
cast (callable): A callable function taking a single value and
|
|
returning a modified value. Useful to turn the configuration
|
|
option into an enum while reading it.
|
|
change_callback(callable): A function that will be called
|
|
by weechat every time the configuration option is changed.
|
|
"""
|
|
|
|
return super().__new__(
|
|
cls,
|
|
name,
|
|
type,
|
|
string_values,
|
|
min,
|
|
max,
|
|
value,
|
|
description,
|
|
cast,
|
|
change_callback,
|
|
)
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_config_reload_cb(data, config_file):
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
def change_log_level(category, level):
|
|
"""Change the log level of the underlying nio lib
|
|
|
|
Called every time the user changes the log level or log category
|
|
configuration option."""
|
|
|
|
if category == "all":
|
|
nio.logger_group.level = level
|
|
elif category == "http":
|
|
nio.http.logger.level = level
|
|
elif category == "client":
|
|
nio.client.logger.level = level
|
|
elif category == "events":
|
|
nio.events.logger.level = level
|
|
elif category == "responses":
|
|
nio.responses.logger.level = level
|
|
elif category == "encryption":
|
|
nio.crypto.logger.level = level
|
|
|
|
|
|
@utf8_decode
|
|
def config_server_buffer_cb(data, option):
|
|
"""Callback for the look.server_buffer option.
|
|
Is called when the option is changed and merges/splits the server
|
|
buffer"""
|
|
|
|
for server in SERVERS.values():
|
|
server.buffer_merge()
|
|
return 1
|
|
|
|
|
|
@utf8_decode
|
|
def config_log_level_cb(data, option):
|
|
"""Callback for the network.debug_level option."""
|
|
change_log_level(
|
|
G.CONFIG.network.debug_category, G.CONFIG.network.debug_level
|
|
)
|
|
return 1
|
|
|
|
|
|
@utf8_decode
|
|
def config_log_category_cb(data, option):
|
|
"""Callback for the network.debug_category option."""
|
|
change_log_level(G.CONFIG.debug_category, logbook.ERROR)
|
|
G.CONFIG.debug_category = G.CONFIG.network.debug_category
|
|
change_log_level(
|
|
G.CONFIG.network.debug_category, G.CONFIG.network.debug_level
|
|
)
|
|
return 1
|
|
|
|
|
|
@utf8_decode
|
|
def config_pgup_cb(data, option):
|
|
"""Callback for the network.fetch_backlog_on_pgup option.
|
|
Enables or disables the hook that is run when /window page_up is called"""
|
|
if G.CONFIG.network.fetch_backlog_on_pgup:
|
|
if not G.CONFIG.page_up_hook:
|
|
G.CONFIG.page_up_hook = W.hook_command_run(
|
|
"/window page_up", "matrix_command_pgup_cb", ""
|
|
)
|
|
else:
|
|
if G.CONFIG.page_up_hook:
|
|
W.unhook(G.CONFIG.page_up_hook)
|
|
G.CONFIG.page_up_hook = None
|
|
|
|
return 1
|
|
|
|
|
|
def level_to_logbook(value):
|
|
if value == 0:
|
|
return logbook.ERROR
|
|
if value == 1:
|
|
return logbook.WARNING
|
|
if value == 2:
|
|
return logbook.INFO
|
|
if value == 3:
|
|
return logbook.DEBUG
|
|
|
|
return logbook.ERROR
|
|
|
|
|
|
def logbook_category(value):
|
|
if value == 0:
|
|
return "all"
|
|
if value == 1:
|
|
return "http"
|
|
if value == 2:
|
|
return "client"
|
|
if value == 3:
|
|
return "events"
|
|
if value == 4:
|
|
return "responses"
|
|
if value == 5:
|
|
return "encryption"
|
|
|
|
return "all"
|
|
|
|
|
|
def eval_cast(string):
|
|
"""A function that passes a string to weechat which evaluates it using its
|
|
expression evaluation syntax.
|
|
Can only be used with strings, useful for passwords or options that contain
|
|
a formatted string to e.g. add colors.
|
|
More info here:
|
|
https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_string_eval_expression"""
|
|
|
|
return W.string_eval_expression(string, {}, {}, {})
|
|
|
|
|
|
class WeechatConfig(object):
|
|
"""A class representing a weechat configuration file
|
|
Wraps weechats configuration creation functionality"""
|
|
|
|
def __init__(self, sections):
|
|
"""Create a new weechat configuration file, expects the global
|
|
SCRIPT_NAME to be defined and a reload callback
|
|
|
|
Parameters:
|
|
sections (List[Tuple[str, List[Option]]]): List of config sections
|
|
that will be created for the configuration file.
|
|
"""
|
|
self._ptr = W.config_new(
|
|
SCRIPT_NAME, SCRIPT_NAME + "_config_reload_cb", ""
|
|
)
|
|
|
|
for section in sections:
|
|
name, options = section
|
|
section_class = ConfigSection.build(name, options)
|
|
setattr(self, name, section_class(name, self._ptr, options))
|
|
|
|
def free(self):
|
|
"""Free all the config sections and their options as well as the
|
|
configuration file. Should be called when the script is unloaded."""
|
|
for section in [
|
|
getattr(self, a)
|
|
for a in dir(self)
|
|
if isinstance(getattr(self, a), ConfigSection)
|
|
]:
|
|
section.free()
|
|
|
|
W.config_free(self._ptr)
|
|
|
|
def read(self):
|
|
"""Read the config file"""
|
|
return_code = W.config_read(self._ptr)
|
|
if return_code == W.WEECHAT_CONFIG_READ_OK:
|
|
return True
|
|
if return_code == W.WEECHAT_CONFIG_READ_MEMORY_ERROR:
|
|
return False
|
|
if return_code == W.WEECHAT_CONFIG_READ_FILE_NOT_FOUND:
|
|
return True
|
|
return False
|
|
|
|
|
|
class ConfigSection(object):
|
|
"""A class representing a weechat config section.
|
|
Should not be used on its own, the WeechatConfig class uses this to build
|
|
config sections."""
|
|
@classmethod
|
|
def build(cls, name, options):
|
|
def constructor(self, name, config_ptr, options):
|
|
self._ptr = W.config_new_section(
|
|
config_ptr, name, 0, 0, "", "", "", "", "", "", "", "", "", ""
|
|
)
|
|
self._config_ptr = config_ptr
|
|
self._option_ptrs = {}
|
|
|
|
for option in options:
|
|
self._add_option(option)
|
|
|
|
attributes = {
|
|
option.name: cls.option_property(
|
|
option.name, option.type, cast_func=option.cast_func
|
|
)
|
|
for option in options
|
|
}
|
|
attributes["__init__"] = constructor
|
|
|
|
section_class = type(name.title() + "Section", (cls,), attributes)
|
|
return section_class
|
|
|
|
def free(self):
|
|
W.config_section_free_options(self._ptr)
|
|
W.config_section_free(self._ptr)
|
|
|
|
def _add_option(self, option):
|
|
cb = option.change_callback.__name__ if option.change_callback else ""
|
|
option_ptr = W.config_new_option(
|
|
self._config_ptr,
|
|
self._ptr,
|
|
option.name,
|
|
option.type,
|
|
option.description,
|
|
option.string_values,
|
|
option.min,
|
|
option.max,
|
|
option.value,
|
|
option.value,
|
|
0,
|
|
"",
|
|
"",
|
|
cb,
|
|
"",
|
|
"",
|
|
"",
|
|
)
|
|
|
|
self._option_ptrs[option.name] = option_ptr
|
|
|
|
@staticmethod
|
|
def option_property(name, option_type, evaluate=False, cast_func=None):
|
|
"""Create a property for this class that makes the reading of config
|
|
option values pythonic. The option will be available as a property with
|
|
the name of the option.
|
|
If a cast function was defined for the option the property will pass
|
|
the option value to the cast function and return its result."""
|
|
|
|
def bool_getter(self):
|
|
return bool(W.config_boolean(self._option_ptrs[name]))
|
|
|
|
def str_getter(self):
|
|
if cast_func:
|
|
return cast_func(W.config_string(self._option_ptrs[name]))
|
|
return W.config_string(self._option_ptrs[name])
|
|
|
|
def str_evaluate_getter(self):
|
|
return W.string_eval_expression(
|
|
W.config_string(self._option_ptrs[name]), {}, {}, {}
|
|
)
|
|
|
|
def int_getter(self):
|
|
if cast_func:
|
|
return cast_func(W.config_integer(self._option_ptrs[name]))
|
|
return W.config_integer(self._option_ptrs[name])
|
|
|
|
if option_type in ("string", "color"):
|
|
if evaluate:
|
|
return property(str_evaluate_getter)
|
|
return property(str_getter)
|
|
if option_type == "boolean":
|
|
return property(bool_getter)
|
|
if option_type == "integer":
|
|
return property(int_getter)
|
|
|
|
|
|
class MatrixConfig(WeechatConfig):
|
|
"""Main matrix configuration file.
|
|
This class defines all the global matrix configuration options.
|
|
New global options should be added to the constructor of this class under
|
|
the appropriate section.
|
|
|
|
There are three main sections defined:
|
|
Look: This section is for options that change the way matrix messages
|
|
are shown or the way the buffers are shown.
|
|
Color: This section should mainly be for color options, options that
|
|
change color schemes or themes should go to the look section.
|
|
Network: This section is for options that change the way the script
|
|
behaves, e.g. the way it communicates with the server, it handles
|
|
responses or any other behavioural change that doesn't fit in the
|
|
previous sections.
|
|
|
|
There is a special section called server defined which contains per server
|
|
configuration options. Server options aren't defined here, they need to be
|
|
added in server.py
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.debug_buffer = ""
|
|
self.upload_buffer = ""
|
|
self.debug_category = "all"
|
|
self.page_up_hook = None
|
|
self.human_buffer_names = None
|
|
|
|
look_options = [
|
|
Option(
|
|
"redactions",
|
|
"integer",
|
|
"strikethrough|notice|delete",
|
|
0,
|
|
0,
|
|
"strikethrough",
|
|
(
|
|
"Only notice redactions, strike through or delete "
|
|
"redacted messages"
|
|
),
|
|
RedactType,
|
|
),
|
|
Option(
|
|
"server_buffer",
|
|
"integer",
|
|
"merge_with_core|merge_without_core|independent",
|
|
0,
|
|
0,
|
|
"merge_with_core",
|
|
"Merge server buffers",
|
|
ServerBufferType,
|
|
config_server_buffer_cb,
|
|
),
|
|
Option(
|
|
"max_typing_notice_item_length",
|
|
"integer",
|
|
"",
|
|
10,
|
|
1000,
|
|
"50",
|
|
("Limit the length of the typing notice bar item."),
|
|
),
|
|
Option(
|
|
"bar_item_typing_notice_prefix",
|
|
"string",
|
|
"",
|
|
0,
|
|
0,
|
|
"Typing: ",
|
|
("Prefix for the typing notice bar item."),
|
|
),
|
|
Option(
|
|
"encryption_warning_sign",
|
|
"string",
|
|
"",
|
|
0,
|
|
0,
|
|
"⚠️ ",
|
|
("A sign that is used to signal trust issues in encrypted "
|
|
"rooms (note: content is evaluated, see /help eval)"),
|
|
eval_cast,
|
|
),
|
|
Option(
|
|
"busy_sign",
|
|
"string",
|
|
"",
|
|
0,
|
|
0,
|
|
"⏳",
|
|
("A sign that is used to signal that the client is busy e.g. "
|
|
"when the room backlog is fetching"
|
|
" (note: content is evaluated, see /help eval)"),
|
|
eval_cast,
|
|
),
|
|
Option(
|
|
"encrypted_room_sign",
|
|
"string",
|
|
"",
|
|
0,
|
|
0,
|
|
"🔐",
|
|
("A sign that is used to show that the current room is "
|
|
"encrypted "
|
|
"(note: content is evaluated, see /help eval)"),
|
|
eval_cast,
|
|
),
|
|
Option(
|
|
"disconnect_sign",
|
|
"string",
|
|
"",
|
|
0,
|
|
0,
|
|
"❌",
|
|
("A sign that is used to show that the server is disconnected "
|
|
"(note: content is evaluated, see /help eval)"),
|
|
eval_cast,
|
|
),
|
|
Option(
|
|
"pygments_style",
|
|
"string",
|
|
"",
|
|
0,
|
|
0,
|
|
"native",
|
|
"Pygments style to use for highlighting source code blocks",
|
|
),
|
|
Option(
|
|
"code_blocks",
|
|
"boolean",
|
|
"",
|
|
0,
|
|
0,
|
|
"on",
|
|
("Display preformatted code blocks as rectangular areas by "
|
|
"padding them with whitespace up to the length of the longest"
|
|
" line (with optional margin)"),
|
|
),
|
|
Option(
|
|
"code_block_margin",
|
|
"integer",
|
|
"",
|
|
0,
|
|
100,
|
|
"2",
|
|
("Number of spaces to add as a margin around around a code "
|
|
"block"),
|
|
),
|
|
Option(
|
|
"human_buffer_names",
|
|
"boolean",
|
|
"",
|
|
0,
|
|
0,
|
|
"off",
|
|
("If turned on the buffer name will consist of the server "
|
|
"name and the room name instead of the Matrix room ID. Note, "
|
|
"this requires a change to the logger.file.mask setting "
|
|
"since conflicts can happen otherwise "
|
|
"(requires a script reload)."),
|
|
),
|
|
]
|
|
|
|
network_options = [
|
|
Option(
|
|
"max_initial_sync_events",
|
|
"integer",
|
|
"",
|
|
1,
|
|
10000,
|
|
"30",
|
|
("How many events to fetch during the initial sync"),
|
|
),
|
|
Option(
|
|
"max_backlog_sync_events",
|
|
"integer",
|
|
"",
|
|
1,
|
|
100,
|
|
"10",
|
|
("How many events to fetch during backlog fetching"),
|
|
),
|
|
Option(
|
|
"fetch_backlog_on_pgup",
|
|
"boolean",
|
|
"",
|
|
0,
|
|
0,
|
|
"on",
|
|
("Fetch messages in the backlog on a window page up event"),
|
|
None,
|
|
config_pgup_cb,
|
|
),
|
|
Option(
|
|
"debug_level",
|
|
"integer",
|
|
"error|warn|info|debug",
|
|
0,
|
|
0,
|
|
"error",
|
|
"Enable network protocol debugging.",
|
|
level_to_logbook,
|
|
config_log_level_cb,
|
|
),
|
|
Option(
|
|
"debug_category",
|
|
"integer",
|
|
"all|http|client|events|responses|encryption",
|
|
0,
|
|
0,
|
|
"all",
|
|
"Debugging category",
|
|
logbook_category,
|
|
config_log_category_cb,
|
|
),
|
|
Option(
|
|
"debug_buffer",
|
|
"boolean",
|
|
"",
|
|
0,
|
|
0,
|
|
"off",
|
|
("Use a separate buffer for debug logs."),
|
|
),
|
|
Option(
|
|
"lazy_load_room_users",
|
|
"boolean",
|
|
"",
|
|
0,
|
|
0,
|
|
"off",
|
|
("If on, room users won't be loaded in the background "
|
|
"proactively, they will be loaded when the user switches to "
|
|
"the room buffer. This only affects non-encrypted rooms."),
|
|
),
|
|
Option(
|
|
"max_nicklist_users",
|
|
"integer",
|
|
"",
|
|
100,
|
|
20000,
|
|
"5000",
|
|
("Limit the number of users that are added to the nicklist. "
|
|
"Active users and users with a higher power level are always."
|
|
" Inactive users will be removed from the nicklist after a "
|
|
"day of inactivity."),
|
|
),
|
|
Option(
|
|
"lag_reconnect",
|
|
"integer",
|
|
"",
|
|
5,
|
|
604800,
|
|
"90",
|
|
("Reconnect to the server if the lag is greater than this "
|
|
"value (in seconds)"),
|
|
),
|
|
Option(
|
|
"print_unconfirmed_messages",
|
|
"boolean",
|
|
"",
|
|
0,
|
|
0,
|
|
"on",
|
|
("If off, messages are only printed after the server confirms "
|
|
"their receival. If on, messages are immediately printed but "
|
|
"colored differently until receival is confirmed."),
|
|
),
|
|
Option(
|
|
"lag_min_show",
|
|
"integer",
|
|
"",
|
|
1,
|
|
604800,
|
|
"500",
|
|
("minimum lag to show (in milliseconds)"),
|
|
),
|
|
Option(
|
|
"typing_notice_conditions",
|
|
"string",
|
|
"",
|
|
0,
|
|
0,
|
|
"${typing_enabled}",
|
|
("conditions to send typing notifications (note: content is "
|
|
"evaluated, see /help eval); besides the buffer and window "
|
|
"variables the typing_enabled variable is also expanded; "
|
|
"the typing_enabled variable can be manipulated with the "
|
|
"/room command, see /help room"),
|
|
),
|
|
Option(
|
|
"read_markers_conditions",
|
|
"string",
|
|
"",
|
|
0,
|
|
0,
|
|
"${markers_enabled}",
|
|
("conditions to send read markers (note: content is "
|
|
"evaluated, see /help eval); besides the buffer and window "
|
|
"variables the markers_enabled variable is also expanded; "
|
|
"the markers_enabled variable can be manipulated with the "
|
|
"/room command, see /help room"),
|
|
),
|
|
Option(
|
|
"resending_ignores_devices",
|
|
"boolean",
|
|
"",
|
|
0,
|
|
0,
|
|
"on",
|
|
("If on resending the same message to a room that contains "
|
|
"unverified devices will mark the devices as ignored and "
|
|
"continue sending the message. If off resending the message "
|
|
"will again fail and devices need to be marked as verified "
|
|
"one by one or the /send-anyways command needs to be used to "
|
|
"ignore them."),
|
|
),
|
|
]
|
|
|
|
color_options = [
|
|
Option(
|
|
"quote_fg",
|
|
"color",
|
|
"",
|
|
0,
|
|
0,
|
|
"lightgreen",
|
|
"Foreground color for matrix style blockquotes",
|
|
),
|
|
Option(
|
|
"quote_bg",
|
|
"color",
|
|
"",
|
|
0,
|
|
0,
|
|
"default",
|
|
"Background counterpart of quote_fg",
|
|
),
|
|
Option(
|
|
"error_message_fg",
|
|
"color",
|
|
"",
|
|
0,
|
|
0,
|
|
"darkgray",
|
|
("Foreground color for error messages that appear inside a "
|
|
"room buffer (e.g. when a message errors out when sending or "
|
|
"when a message is redacted)"),
|
|
),
|
|
Option(
|
|
"error_message_bg",
|
|
"color",
|
|
"",
|
|
0,
|
|
0,
|
|
"default",
|
|
"Background counterpart of error_message_fg.",
|
|
),
|
|
Option(
|
|
"unconfirmed_message_fg",
|
|
"color",
|
|
"",
|
|
0,
|
|
0,
|
|
"darkgray",
|
|
("Foreground color for messages that are printed out but the "
|
|
"server hasn't confirmed the that he received them."),
|
|
),
|
|
Option(
|
|
"unconfirmed_message_bg",
|
|
"color",
|
|
"",
|
|
0,
|
|
0,
|
|
"default",
|
|
"Background counterpart of unconfirmed_message_fg."
|
|
),
|
|
Option(
|
|
"untagged_code_fg",
|
|
"color",
|
|
"",
|
|
0,
|
|
0,
|
|
"blue",
|
|
("Foreground color for code without a language specifier. "
|
|
"Also used for `inline code`."),
|
|
),
|
|
Option(
|
|
"untagged_code_bg",
|
|
"color",
|
|
"",
|
|
0,
|
|
0,
|
|
"default",
|
|
"Background counterpart of untagged_code_fg",
|
|
),
|
|
]
|
|
|
|
sections = [
|
|
("network", network_options),
|
|
("look", look_options),
|
|
("color", color_options),
|
|
]
|
|
|
|
super().__init__(sections)
|
|
|
|
# The server section is essentially a section with subsections and no
|
|
# options, handle that case independently.
|
|
W.config_new_section(
|
|
self._ptr,
|
|
"server",
|
|
0,
|
|
0,
|
|
"matrix_config_server_read_cb",
|
|
"",
|
|
"matrix_config_server_write_cb",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
)
|
|
|
|
def read(self):
|
|
super().read()
|
|
self.human_buffer_names = self.look.human_buffer_names
|
|
|
|
def free(self):
|
|
section_ptr = W.config_search_section(self._ptr, "server")
|
|
W.config_section_free(section_ptr)
|
|
super().free()
|