Merge branch 'develop' into master
This commit is contained in:
commit
6d0b1be448
34 changed files with 353 additions and 152 deletions
|
@ -94,3 +94,6 @@ bitly-api-key =
|
||||||
|
|
||||||
# https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line
|
# https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line
|
||||||
github-token =
|
github-token =
|
||||||
|
|
||||||
|
# https://ipinfo.io/account/token
|
||||||
|
ipinfo-token =
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
|
|
||||||
Generate a TLS keypair and point `bot.conf`'s `tls-key` to the private key and `tls-certificate` to the public key.
|
Generate a TLS keypair and point `bot.conf`'s `tls-key` to the private key and `tls-certificate` to the public key.
|
||||||
|
|
||||||
|
Below is an OpenSSL command example that will create a `bitbot-cert.pem` and `bitbot-key.pem` with `10y` validity (self-signed):
|
||||||
|
> openssl req -x509 -nodes -sha512 -newkey rsa:4096 -keyout bitbot-key.pem -out bitbot-cert.pem -days 3650 -subj "/CN=YourBotNick"
|
||||||
|
|
||||||
### Configure SASL
|
### Configure SASL
|
||||||
|
|
||||||
Configure the bot to use SASL to authenticate (usually used for `NickServ` identification)
|
Configure the bot to use SASL to authenticate (usually used for `NickServ` identification)
|
||||||
|
|
|
@ -15,7 +15,7 @@ Either set up a reverse proxy (with persisted Host header) with your favourite H
|
||||||
#### Apache2
|
#### Apache2
|
||||||
* Run `$ a2enmod ssl proxy proxy_http` as root
|
* Run `$ a2enmod ssl proxy proxy_http` as root
|
||||||
* Copy example config file from [/docs/rest_api/apache2](/docs/rest_api/apache2) to `/etc/apache2/sites-enabled/`
|
* Copy example config file from [/docs/rest_api/apache2](/docs/rest_api/apache2) to `/etc/apache2/sites-enabled/`
|
||||||
* Edit `ServerName`, `SSLCertificateFile and `SSLCertificateKeyFile`
|
* Edit `ServerName`, `SSLCertificateFile` and `SSLCertificateKeyFile`
|
||||||
* `$ service apache2 restart` as root
|
* `$ service apache2 restart` as root
|
||||||
|
|
||||||
#### Lighttpd
|
#### Lighttpd
|
||||||
|
|
|
@ -15,6 +15,6 @@ Listen 5000
|
||||||
|
|
||||||
ProxyRequests off
|
ProxyRequests off
|
||||||
ProxyPass / http://[::1]:5001/
|
ProxyPass / http://[::1]:5001/
|
||||||
ProxyPassReverse / http://[::1]:5001
|
ProxyPassReverse / http://[::1]:5001/
|
||||||
ProxyPreserveHost on
|
ProxyPreserveHost on
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
# which can be disabled with: systemctl --user disable systemd-tmpfiles-clean.timer
|
# which can be disabled with: systemctl --user disable systemd-tmpfiles-clean.timer
|
||||||
#
|
#
|
||||||
# After placing this script in the correct location, and with bitbot stopped, type:
|
# After placing this script in the correct location, and with bitbot stopped, type:
|
||||||
|
# systemcl --user daemon-reload
|
||||||
|
# Afert that start bitbot with:
|
||||||
# systemctl --user enable bitbot_user.service --now
|
# systemctl --user enable bitbot_user.service --now
|
||||||
# This will enable the systemd script and launch bitbot
|
# This will enable the systemd script and launch bitbot
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ REASON = "User is banned from this channel"
|
||||||
|
|
||||||
@utils.export("channelset", utils.BoolSetting("ban-enforce",
|
@utils.export("channelset", utils.BoolSetting("ban-enforce",
|
||||||
"Whether or not to parse new bans and kick who they affect"))
|
"Whether or not to parse new bans and kick who they affect"))
|
||||||
|
@utils.export("channelset", utils.IntSetting("ban-enforce-max",
|
||||||
|
"Do not enforce ban if the ban effects more than this many users. Default is half of total channel users."))
|
||||||
class Module(ModuleManager.BaseModule):
|
class Module(ModuleManager.BaseModule):
|
||||||
@utils.hook("received.mode.channel")
|
@utils.hook("received.mode.channel")
|
||||||
def on_mode(self, event):
|
def on_mode(self, event):
|
||||||
|
@ -14,6 +16,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
if mode[0] == "+" and mode[1] == "b":
|
if mode[0] == "+" and mode[1] == "b":
|
||||||
bans.append(arg)
|
bans.append(arg)
|
||||||
|
|
||||||
|
affected = 0
|
||||||
|
defaultmax = len(event["channel"].users) // 2
|
||||||
|
realmax = event["channel"].get_setting("ban-enforce-max", defaultmax)
|
||||||
|
|
||||||
if bans:
|
if bans:
|
||||||
umasks = {u.hostmask(): u for u in event["channel"].users}
|
umasks = {u.hostmask(): u for u in event["channel"].users}
|
||||||
for ban in bans:
|
for ban in bans:
|
||||||
|
@ -21,7 +27,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
matches = list(utils.irc.hostmask_match_many(
|
matches = list(utils.irc.hostmask_match_many(
|
||||||
umasks.keys(), mask))
|
umasks.keys(), mask))
|
||||||
for match in matches:
|
for match in matches:
|
||||||
|
affected = affected + 1
|
||||||
kicks.add(umasks[match])
|
kicks.add(umasks[match])
|
||||||
if kicks:
|
if kicks:
|
||||||
|
if affected > realmax:
|
||||||
|
return
|
||||||
nicks = [u.nickname for u in kicks]
|
nicks = [u.nickname for u in kicks]
|
||||||
event["channel"].send_kicks(sorted(nicks), REASON)
|
event["channel"].send_kicks(sorted(nicks), REASON)
|
||||||
|
|
|
@ -83,7 +83,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
channel = server.channels.get(channel_name)
|
channel = server.channels.get(channel_name)
|
||||||
|
|
||||||
args = timer.kwargs.get("args", [timer.kwargs.get("arg", None)])
|
args = timer.kwargs.get("args", [timer.kwargs.get("arg", None)])
|
||||||
if args:
|
if any(args):
|
||||||
channel.send_modes(args, False)
|
channel.send_modes(args, False)
|
||||||
else:
|
else:
|
||||||
channel.send_mode(timer.kwargs["mode"], False)
|
channel.send_mode(timer.kwargs["mode"], False)
|
||||||
|
@ -238,7 +238,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
|
|
||||||
if event["spec"][1]:
|
if event["spec"][1]:
|
||||||
self.timers.add_persistent("unmode", event["spec"][1],
|
self.timers.add_persistent("unmode", event["spec"][1],
|
||||||
channel=event["spec"][0].id, mode="m")
|
channel=event["spec"][0].id, mode="-m")
|
||||||
@utils.hook("received.command.cunmute")
|
@utils.hook("received.command.cunmute")
|
||||||
@utils.kwarg("require_mode", "o")
|
@utils.kwarg("require_mode", "o")
|
||||||
@utils.kwarg("require_access", "high,cmute")
|
@utils.kwarg("require_access", "high,cmute")
|
||||||
|
|
|
@ -43,14 +43,22 @@ class Module(ModuleManager.BaseModule):
|
||||||
failed = []
|
failed = []
|
||||||
for list in lists:
|
for list in lists:
|
||||||
record = self._check_list(list.hostname, address)
|
record = self._check_list(list.hostname, address)
|
||||||
if not record == None:
|
if record is not None:
|
||||||
reason = list.process(record) or "unknown"
|
a_record, txt_record = record
|
||||||
|
reason = list.process(a_record, txt_record) or "unknown"
|
||||||
failed.append((list.hostname, reason))
|
failed.append((list.hostname, reason))
|
||||||
return failed
|
return failed
|
||||||
|
|
||||||
def _check_list(self, list, address):
|
def _check_list(self, list, address):
|
||||||
list_address = "%s.%s" % (address, list)
|
list_address = "%s.%s" % (address, list)
|
||||||
try:
|
try:
|
||||||
return dns.resolver.query(list_address, "A")[0].to_text()
|
a_record = dns.resolver.resolve(list_address, "A")[0].to_text()
|
||||||
except dns.resolver.NXDOMAIN:
|
except dns.resolver.NXDOMAIN:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
txt_record = dns.resolver.resolve(list_address, "TXT")[0].to_text()
|
||||||
|
except:
|
||||||
|
txt_record = None
|
||||||
|
|
||||||
|
return (a_record, txt_record)
|
||||||
|
|
|
@ -5,53 +5,62 @@ class DNSBL(object):
|
||||||
if not hostname == None:
|
if not hostname == None:
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
|
|
||||||
def process(self, result: str):
|
def process(self, a_record, txt_record):
|
||||||
return result
|
out = a_record
|
||||||
|
if txt_record is not None:
|
||||||
|
out += f" - {txt_record}"
|
||||||
|
return out
|
||||||
|
|
||||||
class ZenSpamhaus(DNSBL):
|
class ZenSpamhaus(DNSBL):
|
||||||
hostname = "zen.spamhaus.org"
|
hostname = "zen.spamhaus.org"
|
||||||
def process(self, result):
|
def process(self, a_record, txt_record):
|
||||||
result = result.rsplit(".", 1)[1]
|
result = a_record.rsplit(".", 1)[1]
|
||||||
if result in ["2", "3", "9"]:
|
if result in ["2", "3", "9"]:
|
||||||
return "spam"
|
desc = "spam"
|
||||||
elif result in ["4", "5", "6", "7"]:
|
elif result in ["4", "5", "6", "7"]:
|
||||||
return "exploits"
|
desc = "exploits"
|
||||||
|
else:
|
||||||
|
desc = "unknown"
|
||||||
|
return f"{result} - {desc}"
|
||||||
|
|
||||||
class EFNetRBL(DNSBL):
|
class EFNetRBL(DNSBL):
|
||||||
hostname = "rbl.efnetrbl.org"
|
hostname = "rbl.efnetrbl.org"
|
||||||
def process(self, result):
|
def process(self, a_record, txt_record):
|
||||||
result = result.rsplit(".", 1)[1]
|
result = a_record.rsplit(".", 1)[1]
|
||||||
if result == "1":
|
if result == "1":
|
||||||
return "proxy"
|
desc = "proxy"
|
||||||
elif result in ["2", "3"]:
|
elif result in ["2", "3"]:
|
||||||
return "spamtap"
|
desc = "spamtap"
|
||||||
elif result == "4":
|
elif result == "4":
|
||||||
return "tor"
|
desc = "tor"
|
||||||
elif result == "5":
|
elif result == "5":
|
||||||
return "flooding"
|
desc = "flooding"
|
||||||
|
return f"{result} - {desc}"
|
||||||
|
|
||||||
class DroneBL(DNSBL):
|
class DroneBL(DNSBL):
|
||||||
hostname = "dnsbl.dronebl.org"
|
hostname = "dnsbl.dronebl.org"
|
||||||
def process(self, result):
|
|
||||||
result = result.rsplit(".", 1)[1]
|
|
||||||
if result in ["8", "9", "10", "11", "14"]:
|
|
||||||
return "proxy"
|
|
||||||
elif result in ["3", "6", "7"]:
|
|
||||||
return "flooding"
|
|
||||||
elif result in ["12", "13", "15", "16"]:
|
|
||||||
return "exploits"
|
|
||||||
|
|
||||||
class AbuseAtCBL(DNSBL):
|
class AbuseAtCBL(DNSBL):
|
||||||
hostname = "cbl.abuseat.org"
|
hostname = "cbl.abuseat.org"
|
||||||
def process(self, result):
|
def process(self, a_record, txt_record):
|
||||||
result = result.rsplit(".", 1)[1]
|
result = a_record.rsplit(".", 1)[1]
|
||||||
if result == "2":
|
if result == "2":
|
||||||
return "abuse"
|
desc = "abuse"
|
||||||
|
else:
|
||||||
|
desc = "unknown"
|
||||||
|
return f"{result} - {desc}"
|
||||||
|
|
||||||
|
class TorExitDan(DNSBL):
|
||||||
|
hostname = "torexit.dan.me.uk"
|
||||||
|
def process(self, a_record, txt_record):
|
||||||
|
return "tor exit"
|
||||||
|
|
||||||
DEFAULT_LISTS = [
|
DEFAULT_LISTS = [
|
||||||
ZenSpamhaus(),
|
ZenSpamhaus(),
|
||||||
EFNetRBL(),
|
EFNetRBL(),
|
||||||
DroneBL(),
|
DroneBL(),
|
||||||
AbuseAtCBL()
|
AbuseAtCBL(),
|
||||||
|
TorExitDan()
|
||||||
]
|
]
|
||||||
|
|
||||||
def default_lists():
|
def default_lists():
|
||||||
|
|
|
@ -98,6 +98,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
@utils.kwarg("help", "Befriend a duck")
|
@utils.kwarg("help", "Befriend a duck")
|
||||||
@utils.spec("!-channelonly")
|
@utils.spec("!-channelonly")
|
||||||
def befriend(self, event):
|
def befriend(self, event):
|
||||||
|
if not event["target"].get_setting("ducks-enabled", False):
|
||||||
|
return event["stderr"].write(
|
||||||
|
"Ducks are not enabled in this channel"
|
||||||
|
)
|
||||||
if event["target"].duck_active:
|
if event["target"].duck_active:
|
||||||
action = self._duck_action(event["target"], event["user"],
|
action = self._duck_action(event["target"], event["user"],
|
||||||
"befriended", "ducks-befriended")
|
"befriended", "ducks-befriended")
|
||||||
|
@ -109,6 +113,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
@utils.kwarg("help", "Trap a duck")
|
@utils.kwarg("help", "Trap a duck")
|
||||||
@utils.spec("!-channelonly")
|
@utils.spec("!-channelonly")
|
||||||
def trap(self, event):
|
def trap(self, event):
|
||||||
|
if not event["target"].get_setting("ducks-enabled", False):
|
||||||
|
return event["stderr"].write(
|
||||||
|
"Ducks are not enabled in this channel"
|
||||||
|
)
|
||||||
if event["target"].duck_active:
|
if event["target"].duck_active:
|
||||||
action = self._duck_action(event["target"], event["user"],
|
action = self._duck_action(event["target"], event["user"],
|
||||||
"trapped", "ducks-shot")
|
"trapped", "ducks-shot")
|
||||||
|
|
|
@ -31,7 +31,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
_name = "Webhooks"
|
_name = "Webhooks"
|
||||||
|
|
||||||
def on_load(self):
|
def on_load(self):
|
||||||
self._github = github.GitHub(self.log)
|
self._github = github.GitHub(self.log, self.exports)
|
||||||
self._gitea = gitea.Gitea()
|
self._gitea = gitea.Gitea()
|
||||||
self._gitlab = gitlab.GitLab()
|
self._gitlab = gitlab.GitLab()
|
||||||
|
|
||||||
|
@ -135,6 +135,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
for output, url in outputs:
|
for output, url in outputs:
|
||||||
output = "(%s) %s" % (
|
output = "(%s) %s" % (
|
||||||
utils.irc.color(source, colors.COLOR_REPO), output)
|
utils.irc.color(source, colors.COLOR_REPO), output)
|
||||||
|
|
||||||
|
if channel.get_setting("git-prevent-highlight", False):
|
||||||
|
output = self._prevent_highlight(server, channel,
|
||||||
|
output)
|
||||||
|
|
||||||
if url:
|
if url:
|
||||||
if channel.get_setting("git-shorten-urls", False):
|
if channel.get_setting("git-shorten-urls", False):
|
||||||
|
@ -142,10 +146,6 @@ class Module(ModuleManager.BaseModule):
|
||||||
context=channel) or url
|
context=channel) or url
|
||||||
output = "%s - %s" % (output, url)
|
output = "%s - %s" % (output, url)
|
||||||
|
|
||||||
if channel.get_setting("git-prevent-highlight", False):
|
|
||||||
output = self._prevent_highlight(server, channel,
|
|
||||||
output)
|
|
||||||
|
|
||||||
hide_prefix = channel.get_setting("git-hide-prefix", False)
|
hide_prefix = channel.get_setting("git-hide-prefix", False)
|
||||||
self.events.on("send.stdout").call(target=channel,
|
self.events.on("send.stdout").call(target=channel,
|
||||||
module_name=webhook_name, server=server, message=output,
|
module_name=webhook_name, server=server, message=output,
|
||||||
|
@ -228,6 +228,9 @@ class Module(ModuleManager.BaseModule):
|
||||||
if existing_hook:
|
if existing_hook:
|
||||||
raise utils.EventError("There's already a hook for %s" %
|
raise utils.EventError("There's already a hook for %s" %
|
||||||
hook_name)
|
hook_name)
|
||||||
|
if hook_name == None:
|
||||||
|
command = "%s%s" % (event["command_prefix"], event["command"])
|
||||||
|
raise utils.EventError("Not enough arguments (Usage: %s add <hook>)" % command)
|
||||||
|
|
||||||
all_hooks[hook_name] = {
|
all_hooks[hook_name] = {
|
||||||
"events": DEFAULT_EVENT_CATEGORIES.copy(),
|
"events": DEFAULT_EVENT_CATEGORIES.copy(),
|
||||||
|
|
|
@ -5,6 +5,7 @@ COMMIT_URL = "https://github.com/%s/commit/%s"
|
||||||
COMMIT_RANGE_URL = "https://github.com/%s/compare/%s...%s"
|
COMMIT_RANGE_URL = "https://github.com/%s/compare/%s...%s"
|
||||||
CREATE_URL = "https://github.com/%s/tree/%s"
|
CREATE_URL = "https://github.com/%s/tree/%s"
|
||||||
|
|
||||||
|
PR_URL = "https://github.com/%s/pull/%s"
|
||||||
PR_COMMIT_RANGE_URL = "https://github.com/%s/pull/%s/files/%s..%s"
|
PR_COMMIT_RANGE_URL = "https://github.com/%s/pull/%s/files/%s..%s"
|
||||||
PR_COMMIT_URL = "https://github.com/%s/pull/%s/commits/%s"
|
PR_COMMIT_URL = "https://github.com/%s/pull/%s/commits/%s"
|
||||||
|
|
||||||
|
@ -77,19 +78,19 @@ COMMENT_ACTIONS = {
|
||||||
}
|
}
|
||||||
COMMENT_MAX = 100
|
COMMENT_MAX = 100
|
||||||
|
|
||||||
CHECK_RUN_CONCLUSION = {
|
CHECK_SUITE_CONCLUSION = {
|
||||||
"success": "passed",
|
"success": ("passed", colors.COLOR_POSITIVE),
|
||||||
"failure": "failed",
|
"failure": ("failed", colors.COLOR_NEGATIVE),
|
||||||
"neutral": "finished",
|
"neutral": ("finished", colors.COLOR_NEUTRAL),
|
||||||
"cancelled": "was cancelled",
|
"cancelled": ("was cancelled", colors.COLOR_NEGATIVE),
|
||||||
"timed_out": "timed out",
|
"timed_out": ("timed out", colors.COLOR_NEGATIVE),
|
||||||
"action_required": "requires action"
|
"action_required": ("requires action", colors.COLOR_NEUTRAL)
|
||||||
}
|
}
|
||||||
CHECK_RUN_FAILURES = ["failure", "cancelled", "timed_out", "action_required"]
|
|
||||||
|
|
||||||
class GitHub(object):
|
class GitHub(object):
|
||||||
def __init__(self, log):
|
def __init__(self, log, exports):
|
||||||
self.log = log
|
self.log = log
|
||||||
|
self.exports = exports
|
||||||
|
|
||||||
def is_private(self, data, headers):
|
def is_private(self, data, headers):
|
||||||
if "repository" in data:
|
if "repository" in data:
|
||||||
|
@ -125,6 +126,8 @@ class GitHub(object):
|
||||||
category_action = None
|
category_action = None
|
||||||
if "review" in data and "state" in data["review"]:
|
if "review" in data and "state" in data["review"]:
|
||||||
category = "%s+%s" % (event, data["review"]["state"])
|
category = "%s+%s" % (event, data["review"]["state"])
|
||||||
|
elif "check_suite" in data and "conclusion" in data["check_suite"]:
|
||||||
|
category = "%s+%s" % (event, data["check_suite"]["conclusion"])
|
||||||
|
|
||||||
if action:
|
if action:
|
||||||
if category:
|
if category:
|
||||||
|
@ -159,8 +162,8 @@ class GitHub(object):
|
||||||
out = self.delete(full_name, data)
|
out = self.delete(full_name, data)
|
||||||
elif event == "release":
|
elif event == "release":
|
||||||
out = self.release(full_name, data)
|
out = self.release(full_name, data)
|
||||||
elif event == "check_run":
|
elif event == "check_suite":
|
||||||
out = self.check_run(data)
|
out = self.check_suite(full_name, data)
|
||||||
elif event == "fork":
|
elif event == "fork":
|
||||||
out = self.fork(full_name, data)
|
out = self.fork(full_name, data)
|
||||||
elif event == "ping":
|
elif event == "ping":
|
||||||
|
@ -172,15 +175,10 @@ class GitHub(object):
|
||||||
return list(zip(out, [None]*len(out)))
|
return list(zip(out, [None]*len(out)))
|
||||||
|
|
||||||
def _short_url(self, url):
|
def _short_url(self, url):
|
||||||
self.log.debug("git.io shortening: %s" % url)
|
# TODO: find an alternative to git.io
|
||||||
try:
|
# see https://github.com/jesopo/bitbot/issues/338
|
||||||
page = utils.http.request("https://git.io", method="POST",
|
# ~ examknow 1/19/2022
|
||||||
post_data={"url": url})
|
return url
|
||||||
return page.headers["Location"]
|
|
||||||
except utils.http.HTTPTimeoutException:
|
|
||||||
self.log.warn(
|
|
||||||
"HTTPTimeoutException while waiting for github short URL", [])
|
|
||||||
return url
|
|
||||||
|
|
||||||
def _iso8601(self, s):
|
def _iso8601(self, s):
|
||||||
return utils.datetime.parse.iso8601(s)
|
return utils.datetime.parse.iso8601(s)
|
||||||
|
@ -272,7 +270,7 @@ class GitHub(object):
|
||||||
colored_branch = utils.irc.color(branch, colors.COLOR_BRANCH)
|
colored_branch = utils.irc.color(branch, colors.COLOR_BRANCH)
|
||||||
sender = utils.irc.bold(data["sender"]["login"])
|
sender = utils.irc.bold(data["sender"]["login"])
|
||||||
|
|
||||||
author = utils.irc.bold(data["sender"]["login"])
|
author = utils.irc.bold(data["pull_request"]["user"]["login"])
|
||||||
number = utils.irc.color("#%s" % data["pull_request"]["number"],
|
number = utils.irc.color("#%s" % data["pull_request"]["number"],
|
||||||
colors.COLOR_ID)
|
colors.COLOR_ID)
|
||||||
identifier = "%s by %s" % (number, author)
|
identifier = "%s by %s" % (number, author)
|
||||||
|
@ -433,44 +431,32 @@ class GitHub(object):
|
||||||
url = self._short_url(data["release"]["html_url"])
|
url = self._short_url(data["release"]["html_url"])
|
||||||
return ["%s %s a release%s - %s" % (author, action, name, url)]
|
return ["%s %s a release%s - %s" % (author, action, name, url)]
|
||||||
|
|
||||||
def check_run(self, data):
|
def check_suite(self, full_name, data):
|
||||||
name = data["check_run"]["name"]
|
suite = data["check_suite"]
|
||||||
commit = self._short_hash(data["check_run"]["head_sha"])
|
|
||||||
|
commit = self._short_hash(suite["head_sha"])
|
||||||
commit = utils.irc.color(commit, utils.consts.LIGHTBLUE)
|
commit = utils.irc.color(commit, utils.consts.LIGHTBLUE)
|
||||||
|
|
||||||
|
pr = ""
|
||||||
url = ""
|
url = ""
|
||||||
if data["check_run"]["details_url"]:
|
if suite["pull_requests"]:
|
||||||
url = data["check_run"]["details_url"]
|
pr_num = suite["pull_requests"][0]["number"]
|
||||||
url = " - %s" % self.exports.get("shorturl-any")(url)
|
pr = "/PR%s" % utils.irc.color("#%s" % pr_num, colors.COLOR_ID)
|
||||||
|
url = self._short_url(PR_URL % (full_name, pr_num))
|
||||||
|
url = " - %s" % url
|
||||||
|
|
||||||
duration = ""
|
name = suite["app"]["name"]
|
||||||
if data["check_run"]["completed_at"]:
|
conclusion = suite["conclusion"]
|
||||||
started_at = self._iso8601(data["check_run"]["started_at"])
|
conclusion, conclusion_color = CHECK_SUITE_CONCLUSION[conclusion]
|
||||||
completed_at = self._iso8601(data["check_run"]["completed_at"])
|
conclusion = utils.irc.color(conclusion, conclusion_color)
|
||||||
if completed_at > started_at:
|
|
||||||
seconds = (completed_at-started_at).total_seconds()
|
|
||||||
duration = " in %s" % utils.datetime.format.to_pretty_time(
|
|
||||||
seconds)
|
|
||||||
|
|
||||||
status = data["check_run"]["status"]
|
created_at = self._iso8601(suite["created_at"])
|
||||||
status_str = ""
|
updated_at = self._iso8601(suite["updated_at"])
|
||||||
if status == "queued":
|
seconds = (updated_at-created_at).total_seconds()
|
||||||
status_str = utils.irc.bold("queued")
|
duration = utils.datetime.format.to_pretty_time(seconds)
|
||||||
elif status == "in_progress":
|
|
||||||
status_str = utils.irc.bold("started")
|
|
||||||
elif status == "completed":
|
|
||||||
conclusion = data["check_run"]["conclusion"]
|
|
||||||
conclusion_color = colors.COLOR_POSITIVE
|
|
||||||
if conclusion in CHECK_RUN_FAILURES:
|
|
||||||
conclusion_color = colors.COLOR_NEGATIVE
|
|
||||||
if conclusion == "neutral":
|
|
||||||
conclusion_color = colors.COLOR_NEUTRAL
|
|
||||||
|
|
||||||
status_str = utils.irc.color(
|
return ["[build @%s%s] %s: %s in %s%s" % (
|
||||||
CHECK_RUN_CONCLUSION[conclusion], conclusion_color)
|
commit, pr, name, conclusion, duration, url)]
|
||||||
|
|
||||||
return ["[build @%s] %s: %s%s%s" % (
|
|
||||||
commit, name, status_str, duration, url)]
|
|
||||||
|
|
||||||
def fork(self, full_name, data):
|
def fork(self, full_name, data):
|
||||||
forker = utils.irc.bold(data["sender"]["login"])
|
forker = utils.irc.bold(data["sender"]["login"])
|
||||||
|
|
|
@ -47,14 +47,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
return org, repo, number
|
return org, repo, number
|
||||||
|
|
||||||
def _short_url(self, url):
|
def _short_url(self, url):
|
||||||
try:
|
# TODO: find an alternative to git.io
|
||||||
page = utils.http.request("https://git.io", method="POST",
|
# see https://github.com/jesopo/bitbot/issues/338
|
||||||
post_data={"url": url})
|
# ~ examknow 1/19/2022
|
||||||
return page.headers["Location"]
|
return url
|
||||||
except utils.http.HTTPTimeoutException:
|
|
||||||
self.log.warn(
|
|
||||||
"HTTPTimeoutException while waiting for github short URL", [])
|
|
||||||
return url
|
|
||||||
|
|
||||||
def _change_count(self, n, symbol, color):
|
def _change_count(self, n, symbol, color):
|
||||||
return utils.irc.color("%s%d" % (symbol, n), color)+utils.irc.bold("")
|
return utils.irc.color("%s%d" % (symbol, n), color)+utils.irc.bold("")
|
||||||
|
|
|
@ -5,6 +5,7 @@ from src import ModuleManager, utils
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
|
|
||||||
URL_GEOIP = "http://ip-api.com/json/%s"
|
URL_GEOIP = "http://ip-api.com/json/%s"
|
||||||
|
URL_IPINFO = "https://ipinfo.io/%s/json"
|
||||||
REGEX_IPv6 = r"(?:(?:[a-f0-9]{1,4}:){2,}|[a-f0-9:]*::)[a-f0-9:]*"
|
REGEX_IPv6 = r"(?:(?:[a-f0-9]{1,4}:){2,}|[a-f0-9:]*::)[a-f0-9:]*"
|
||||||
REGEX_IPv4 = r"(?:\d{1,3}\.){3}\d{1,3}"
|
REGEX_IPv4 = r"(?:\d{1,3}\.){3}\d{1,3}"
|
||||||
REGEX_IP = re.compile("%s|%s" % (REGEX_IPv4, REGEX_IPv6), re.I)
|
REGEX_IP = re.compile("%s|%s" % (REGEX_IPv4, REGEX_IPv6), re.I)
|
||||||
|
@ -21,6 +22,25 @@ def _parse(value):
|
||||||
@utils.export("channelset", utils.FunctionSetting(_parse, "dns-nameserver",
|
@utils.export("channelset", utils.FunctionSetting(_parse, "dns-nameserver",
|
||||||
"Set DNS nameserver", example="8.8.8.8"))
|
"Set DNS nameserver", example="8.8.8.8"))
|
||||||
class Module(ModuleManager.BaseModule):
|
class Module(ModuleManager.BaseModule):
|
||||||
|
def _get_ip(self, event):
|
||||||
|
ip = event["args_split"][0] if event["args"] else ""
|
||||||
|
if not ip:
|
||||||
|
line = event["target"].buffer.find(REGEX_IP)
|
||||||
|
if line:
|
||||||
|
ip = line.match
|
||||||
|
if not ip:
|
||||||
|
raise utils.EventError("No IP provided")
|
||||||
|
return ip
|
||||||
|
|
||||||
|
def _ipinfo_get(self, url):
|
||||||
|
access_token = self.bot.config.get("ipinfo-token", None)
|
||||||
|
headers = {}
|
||||||
|
if not access_token == None:
|
||||||
|
headers["Authorization"] = "Bearer %s" % access_token
|
||||||
|
request = utils.http.Request(url, headers=headers)
|
||||||
|
return utils.http.request(request)
|
||||||
|
|
||||||
|
@utils.hook("received.command.dig", alias_of="dns")
|
||||||
@utils.hook("received.command.dns", min_args=1)
|
@utils.hook("received.command.dns", min_args=1)
|
||||||
def dns(self, event):
|
def dns(self, event):
|
||||||
"""
|
"""
|
||||||
|
@ -55,7 +75,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
for record_type in record_types:
|
for record_type in record_types:
|
||||||
record_type_strip = record_type.rstrip("?").upper()
|
record_type_strip = record_type.rstrip("?").upper()
|
||||||
try:
|
try:
|
||||||
query_result = resolver.query(hostname, record_type_strip,
|
query_result = resolver.resolve(hostname, record_type_strip,
|
||||||
lifetime=4)
|
lifetime=4)
|
||||||
query_results = [q.to_text() for q in query_result]
|
query_results = [q.to_text() for q in query_result]
|
||||||
results.append([record_type_strip, query_result.rrset.ttl,
|
results.append([record_type_strip, query_result.rrset.ttl,
|
||||||
|
@ -78,27 +98,79 @@ class Module(ModuleManager.BaseModule):
|
||||||
(t, ttl, ", ".join(r)) for t, ttl, r in results]
|
(t, ttl, ", ".join(r)) for t, ttl, r in results]
|
||||||
event["stdout"].write("(%s) %s" % (hostname, " | ".join(results_str)))
|
event["stdout"].write("(%s) %s" % (hostname, " | ".join(results_str)))
|
||||||
|
|
||||||
@utils.hook("received.command.geoip", min_args=1)
|
@utils.hook("received.command.geoip")
|
||||||
def geoip(self, event):
|
def geoip(self, event):
|
||||||
"""
|
"""
|
||||||
:help: Get geoip data on a given IPv4/IPv6 address
|
:help: Get GeoIP data on a given IPv4/IPv6 address
|
||||||
:usage: <IP>
|
:usage: <IP>
|
||||||
:prefix: GeoIP
|
:prefix: GeoIP
|
||||||
"""
|
"""
|
||||||
page = utils.http.request(URL_GEOIP % event["args_split"][0]).json()
|
ip = self._get_ip(event)
|
||||||
|
|
||||||
|
page = utils.http.request(URL_GEOIP % ip).json()
|
||||||
if page:
|
if page:
|
||||||
if page["status"] == "success":
|
if page["status"] == "success":
|
||||||
|
hostname = None
|
||||||
|
try:
|
||||||
|
hostname, alias, ips = socket.gethostbyaddr(page["query"])
|
||||||
|
except (socket.herror, socket.gaierror):
|
||||||
|
pass
|
||||||
|
|
||||||
data = page["query"]
|
data = page["query"]
|
||||||
|
data += " (%s)" % hostname if hostname else ""
|
||||||
data += " | Organisation: %s" % page["org"]
|
data += " | Organisation: %s" % page["org"]
|
||||||
data += " | City: %s" % page["city"]
|
data += " | City: %s" % page["city"]
|
||||||
data += " | Region: %s (%s)" % (
|
data += " | Region: %s (%s)" % (
|
||||||
page["regionName"], page["countryCode"])
|
page["regionName"], page["countryCode"])
|
||||||
data += " | ISP: %s" % page["isp"]
|
data += " | ISP: %s (%s)" % (page["isp"], page["as"])
|
||||||
data += " | Lon/Lat: %s/%s" % (page["lon"], page["lat"])
|
data += " | Lon/Lat: %s/%s" % (page["lon"], page["lat"])
|
||||||
data += " | Timezone: %s" % page["timezone"]
|
data += " | Timezone: %s" % page["timezone"]
|
||||||
event["stdout"].write(data)
|
event["stdout"].write(data)
|
||||||
else:
|
else:
|
||||||
event["stderr"].write("No geoip data found")
|
event["stderr"].write("No GeoIP data found")
|
||||||
|
else:
|
||||||
|
raise utils.EventResultsError()
|
||||||
|
|
||||||
|
@utils.hook("received.command.ipinfo")
|
||||||
|
def ipinfo(self, event):
|
||||||
|
"""
|
||||||
|
:help: Get IPinfo.io data on a given IPv4/IPv6 address
|
||||||
|
:usage: <IP>
|
||||||
|
:prefix: IPinfo
|
||||||
|
"""
|
||||||
|
ip = self._get_ip(event)
|
||||||
|
|
||||||
|
page = self._ipinfo_get(URL_IPINFO % ip).json()
|
||||||
|
if page:
|
||||||
|
if page.get("error", False):
|
||||||
|
if isinstance(page["error"], (list, dict)):
|
||||||
|
event["stderr"].write(page["error"]["message"])
|
||||||
|
else:
|
||||||
|
event["stderr"].write(page["error"])
|
||||||
|
elif page.get("ip", False):
|
||||||
|
bogon = page.get("bogon", False)
|
||||||
|
hostname = page.get("hostname", None)
|
||||||
|
if not hostname and not bogon:
|
||||||
|
try:
|
||||||
|
hostname, alias, ips = socket.gethostbyaddr(page["ip"])
|
||||||
|
except (socket.herror, socket.gaierror):
|
||||||
|
pass
|
||||||
|
|
||||||
|
data = page["ip"]
|
||||||
|
if bogon:
|
||||||
|
data += " (Bogon)"
|
||||||
|
else:
|
||||||
|
data += " (%s)" % hostname if hostname else ""
|
||||||
|
data += " (Anycast)" if page.get("anycast", False) == True else ""
|
||||||
|
if page.get("country", False):
|
||||||
|
data += " | City: %s" % page["city"]
|
||||||
|
data += " | Region: %s (%s)" % (page["region"], page["country"])
|
||||||
|
data += " | ISP: %s" % page.get("org", "Unknown")
|
||||||
|
data += " | Lon/Lat: %s" % page["loc"]
|
||||||
|
data += " | Timezone: %s" % page["timezone"]
|
||||||
|
event["stdout"].write(data)
|
||||||
|
else:
|
||||||
|
event["stderr"].write("Unsupported endpoint")
|
||||||
else:
|
else:
|
||||||
raise utils.EventResultsError()
|
raise utils.EventResultsError()
|
||||||
|
|
||||||
|
@ -109,13 +181,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
:usage: <IP>
|
:usage: <IP>
|
||||||
:prefix: rDNS
|
:prefix: rDNS
|
||||||
"""
|
"""
|
||||||
ip = event["args_split"][0] if event["args"] else ""
|
ip = self._get_ip(event)
|
||||||
if not ip:
|
|
||||||
line = event["target"].buffer.find(REGEX_IP)
|
|
||||||
if line:
|
|
||||||
ip = line.match
|
|
||||||
if not ip:
|
|
||||||
raise utils.EventError("No IP provided")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
hostname, alias, ips = socket.gethostbyaddr(ip)
|
hostname, alias, ips = socket.gethostbyaddr(ip)
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
from src import EventManager, ModuleManager, utils
|
from src import EventManager, ModuleManager, utils
|
||||||
|
|
||||||
TAG = utils.irc.MessageTag(None, "inspircd.org/bot")
|
TAGS = {
|
||||||
|
utils.irc.MessageTag(None, "inspircd.org/bot"),
|
||||||
|
utils.irc.MessageTag(None, "draft/bot")
|
||||||
|
}
|
||||||
|
|
||||||
class Module(ModuleManager.BaseModule):
|
class Module(ModuleManager.BaseModule):
|
||||||
|
@utils.hook("received.376")
|
||||||
|
@utils.hook("received.422")
|
||||||
|
def botmode(self, event):
|
||||||
|
if "BOT" in event["server"].isupport:
|
||||||
|
botmode = event["server"].isupport["BOT"]
|
||||||
|
event["server"].send_raw("MODE %s +%s" % (event["server"].nickname, botmode))
|
||||||
|
|
||||||
@utils.hook("received.message.private")
|
@utils.hook("received.message.private")
|
||||||
@utils.hook("received.message.channel")
|
@utils.hook("received.message.channel")
|
||||||
@utils.kwarg("priority", EventManager.PRIORITY_HIGH)
|
@utils.kwarg("priority", EventManager.PRIORITY_HIGH)
|
||||||
def message(self, event):
|
def message(self, event):
|
||||||
if TAG.present(event["tags"]):
|
for tag in TAGS:
|
||||||
event.eat()
|
if tag.present(event["tags"]):
|
||||||
|
event.eat()
|
||||||
|
|
|
@ -14,6 +14,18 @@ REGEX_PARENS = re.compile(r"\(([^)]+)\)(\+\+|--)")
|
||||||
@utils.export("channelset", utils.BoolSetting("karma-pattern",
|
@utils.export("channelset", utils.BoolSetting("karma-pattern",
|
||||||
"Enable/disable parsing ++/-- karma format"))
|
"Enable/disable parsing ++/-- karma format"))
|
||||||
class Module(ModuleManager.BaseModule):
|
class Module(ModuleManager.BaseModule):
|
||||||
|
def listify(self, items):
|
||||||
|
if type(items) != list:
|
||||||
|
items = list(items)
|
||||||
|
listified = ""
|
||||||
|
if len(items) > 2:
|
||||||
|
listified = ', '.join(items[:-1]) + ', and ' + items[-1]
|
||||||
|
elif len(items) > 1:
|
||||||
|
listified = items[0] + ' and ' + items[1]
|
||||||
|
elif items:
|
||||||
|
listified = items[0]
|
||||||
|
return listified
|
||||||
|
|
||||||
def _karma_str(self, karma):
|
def _karma_str(self, karma):
|
||||||
karma_str = str(karma)
|
karma_str = str(karma)
|
||||||
if karma < 0:
|
if karma < 0:
|
||||||
|
@ -66,7 +78,8 @@ class Module(ModuleManager.BaseModule):
|
||||||
self._set_throttle(sender, positive)
|
self._set_throttle(sender, positive)
|
||||||
karma_str = self._karma_str(karma)
|
karma_str = self._karma_str(karma)
|
||||||
|
|
||||||
karma_total = self._karma_str(self._get_karma(server, target))
|
karma_total = sum(self._get_karma(server, target).values())
|
||||||
|
karma_total = self._karma_str(karma_total)
|
||||||
|
|
||||||
return True, "%s now has %s karma (%s from %s)" % (
|
return True, "%s now has %s karma (%s from %s)" % (
|
||||||
target, karma_total, karma_str, sender.nickname)
|
target, karma_total, karma_str, sender.nickname)
|
||||||
|
@ -118,18 +131,35 @@ class Module(ModuleManager.BaseModule):
|
||||||
target = event["user"].nickname
|
target = event["user"].nickname
|
||||||
|
|
||||||
target = self._get_target(event["server"], target)
|
target = self._get_target(event["server"], target)
|
||||||
karma = self._karma_str(self._get_karma(event["server"], target))
|
karma = sum(self._get_karma(event["server"], target).values())
|
||||||
|
karma = self._karma_str(karma)
|
||||||
|
|
||||||
event["stdout"].write("%s has %s karma" % (target, karma))
|
event["stdout"].write("%s has %s karma" % (target, karma))
|
||||||
|
|
||||||
def _get_karma(self, server, target):
|
@utils.hook("received.command.karmawho")
|
||||||
|
@utils.spec("!<target>string")
|
||||||
|
def karmawho(self, event):
|
||||||
|
target = event["server"].irc_lower(event["spec"][0])
|
||||||
|
karma = self._get_karma(event["server"], target, True)
|
||||||
|
karma = sorted(list(karma.items()),
|
||||||
|
key=lambda k: abs(k[1]),
|
||||||
|
reverse=True)
|
||||||
|
|
||||||
|
parts = ["%s (%d)" % (n, v) for n, v in karma]
|
||||||
|
if len(parts) == 0:
|
||||||
|
event["stdout"].write("%s has no karma." % target)
|
||||||
|
return
|
||||||
|
event["stdout"].write("%s has karma from: %s" %
|
||||||
|
(target, self.listify(parts)))
|
||||||
|
|
||||||
|
def _get_karma(self, server, target, own=False):
|
||||||
settings = dict(server.get_all_user_settings("karma-%s" % target))
|
settings = dict(server.get_all_user_settings("karma-%s" % target))
|
||||||
|
|
||||||
target_lower = server.irc_lower(target)
|
target_lower = server.irc_lower(target)
|
||||||
if target_lower in settings:
|
if target_lower in settings and not own:
|
||||||
del settings[target_lower]
|
del settings[target_lower]
|
||||||
|
|
||||||
return sum(settings.values())
|
return settings
|
||||||
|
|
||||||
@utils.hook("received.command.resetkarma")
|
@utils.hook("received.command.resetkarma")
|
||||||
@utils.kwarg("min_args", 2)
|
@utils.kwarg("min_args", 2)
|
||||||
|
|
|
@ -82,7 +82,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
|
|
||||||
tags_str = ""
|
tags_str = ""
|
||||||
if "toptags" in track and track["toptags"]["tag"]:
|
if "toptags" in track and track["toptags"]["tag"]:
|
||||||
tags = [t["name"] for t in track["toptags"]["tag"]]
|
tags_list = track["toptags"]["tag"]
|
||||||
|
if not type(tags_list) == list:
|
||||||
|
tags_list = [tags_list]
|
||||||
|
tags = [t["name"] for t in tags_list]
|
||||||
tags_str = " [%s]" % ", ".join(tags)
|
tags_str = " [%s]" % ", ".join(tags)
|
||||||
|
|
||||||
play_count_str = ""
|
play_count_str = ""
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#--require-config opencagedata-api-key
|
#--require-config opencagedata-api-key
|
||||||
|
|
||||||
import typing
|
import typing
|
||||||
|
import pytz
|
||||||
from src import ModuleManager, utils
|
from src import ModuleManager, utils
|
||||||
|
|
||||||
URL_OPENCAGE = "https://api.opencagedata.com/geocode/v1/json"
|
URL_OPENCAGE = "https://api.opencagedata.com/geocode/v1/json"
|
||||||
|
@ -19,6 +20,11 @@ class Module(ModuleManager.BaseModule):
|
||||||
if page and page["results"]:
|
if page and page["results"]:
|
||||||
result = page["results"][0]
|
result = page["results"][0]
|
||||||
timezone = result["annotations"]["timezone"]["name"]
|
timezone = result["annotations"]["timezone"]["name"]
|
||||||
|
try:
|
||||||
|
pytz.timezone(timezone)
|
||||||
|
except pytz.exceptions.UnknownTimeZoneError:
|
||||||
|
return None
|
||||||
|
|
||||||
lat = result["geometry"]["lat"]
|
lat = result["geometry"]["lat"]
|
||||||
lon = result["geometry"]["lng"]
|
lon = result["geometry"]["lng"]
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
@utils.hook("preprocess.send.privmsg")
|
@utils.hook("preprocess.send.privmsg")
|
||||||
@utils.hook("preprocess.send.notice")
|
@utils.hook("preprocess.send.notice")
|
||||||
def channel_message(self, event):
|
def channel_message(self, event):
|
||||||
|
if event["line"].assured():
|
||||||
|
# don't run filters/replaces against assured lines
|
||||||
|
return
|
||||||
|
|
||||||
message = event["line"].args[1]
|
message = event["line"].args[1]
|
||||||
original_message = message
|
original_message = message
|
||||||
message_plain = utils.irc.strip_font(message)
|
message_plain = utils.irc.strip_font(message)
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
from src import ModuleManager, utils
|
from src import ModuleManager, utils
|
||||||
|
|
||||||
@utils.export("set", utils.Setting("pronouns", "Set your pronouns",
|
@utils.export("set", utils.Setting("pronouns", "Set your pronouns",
|
||||||
example="she/her"))
|
example="they/them"))
|
||||||
class Module(ModuleManager.BaseModule):
|
class Module(ModuleManager.BaseModule):
|
||||||
@utils.hook("received.command.pronouns")
|
@utils.hook("received.command.pronouns")
|
||||||
def pronouns(self, event):
|
def pronouns(self, event):
|
||||||
|
|
|
@ -5,6 +5,9 @@ from src import ModuleManager, utils
|
||||||
|
|
||||||
@utils.export("channelset", utils.BoolSetting("channel-quotes",
|
@utils.export("channelset", utils.BoolSetting("channel-quotes",
|
||||||
"Whether or not quotes added from this channel are kept in this channel"))
|
"Whether or not quotes added from this channel are kept in this channel"))
|
||||||
|
@utils.export("set", utils.BoolSetting("quotable",
|
||||||
|
"Whether or not you wish to be quoted"))
|
||||||
|
|
||||||
class Module(ModuleManager.BaseModule):
|
class Module(ModuleManager.BaseModule):
|
||||||
def category_and_quote(self, s):
|
def category_and_quote(self, s):
|
||||||
category, sep, quote = s.partition("=")
|
category, sep, quote = s.partition("=")
|
||||||
|
@ -31,6 +34,11 @@ class Module(ModuleManager.BaseModule):
|
||||||
"channel-quotes", False):
|
"channel-quotes", False):
|
||||||
target = event["target"]
|
target = event["target"]
|
||||||
|
|
||||||
|
if not event["server"].get_user(category).get_setting(
|
||||||
|
"quotable", True):
|
||||||
|
event["stderr"].write("%s does not wish to be quoted" % category)
|
||||||
|
return
|
||||||
|
|
||||||
quotes = self._get_quotes(target, category)
|
quotes = self._get_quotes(target, category)
|
||||||
quotes.append([event["user"].name, int(time.time()), quote])
|
quotes.append([event["user"].name, int(time.time()), quote])
|
||||||
self._set_quotes(target, category, quotes)
|
self._set_quotes(target, category, quotes)
|
||||||
|
@ -148,6 +156,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
text = " ".join(lines_str)
|
text = " ".join(lines_str)
|
||||||
|
|
||||||
quote_category = line.sender
|
quote_category = line.sender
|
||||||
|
if not event["server"].get_user(quote_category).get_setting(
|
||||||
|
"quotable", True):
|
||||||
|
event["stderr"].write("%s does not wish to be quoted" % quote_category)
|
||||||
|
return
|
||||||
if event["server"].has_user(quote_category):
|
if event["server"].has_user(quote_category):
|
||||||
quote_category = event["server"].get_user_nickname(
|
quote_category = event["server"].get_user_nickname(
|
||||||
event["server"].get_user(quote_category).get_id())
|
event["server"].get_user(quote_category).get_id())
|
||||||
|
|
|
@ -9,11 +9,11 @@ RSS_INTERVAL = 60 # 1 minute
|
||||||
|
|
||||||
SETTING_BIND = utils.Setting("rss-bindhost",
|
SETTING_BIND = utils.Setting("rss-bindhost",
|
||||||
"Which local address to bind to for RSS requests", example="127.0.0.1")
|
"Which local address to bind to for RSS requests", example="127.0.0.1")
|
||||||
|
|
||||||
@utils.export("botset", utils.IntSetting("rss-interval",
|
@utils.export("botset", utils.IntSetting("rss-interval",
|
||||||
"Interval (in seconds) between RSS polls", example="120"))
|
"Interval (in seconds) between RSS polls", example="120"))
|
||||||
@utils.export("channelset", utils.BoolSetting("rss-shorten",
|
@utils.export("channelset", utils.BoolSetting("rss-shorten",
|
||||||
"Whether or not to shorten RSS urls"))
|
"Whether or not to shorten RSS urls"))
|
||||||
|
@utils.export("channelset", utils.Setting("rss-format", "Format of RSS announcements", example="$longtitle: $title - $link [$author]"))
|
||||||
@utils.export("serverset", SETTING_BIND)
|
@utils.export("serverset", SETTING_BIND)
|
||||||
@utils.export("channelset", SETTING_BIND)
|
@utils.export("channelset", SETTING_BIND)
|
||||||
class Module(ModuleManager.BaseModule):
|
class Module(ModuleManager.BaseModule):
|
||||||
|
@ -22,12 +22,12 @@ class Module(ModuleManager.BaseModule):
|
||||||
self.timers.add("rss-feeds", self._timer,
|
self.timers.add("rss-feeds", self._timer,
|
||||||
self.bot.get_setting("rss-interval", RSS_INTERVAL))
|
self.bot.get_setting("rss-interval", RSS_INTERVAL))
|
||||||
|
|
||||||
def _format_entry(self, server, feed_title, entry, shorten):
|
def _format_entry(self, server, channel, feed_title, entry, shorten):
|
||||||
title = utils.parse.line_normalise(utils.http.strip_html(
|
title = utils.parse.line_normalise(utils.http.strip_html(
|
||||||
entry["title"]))
|
entry["title"]))
|
||||||
|
|
||||||
author = entry.get("author", None)
|
author = entry.get("author", "unknown author")
|
||||||
author = " by %s" % author if author else ""
|
author = "%s" % author if author else ""
|
||||||
|
|
||||||
link = entry.get("link", None)
|
link = entry.get("link", None)
|
||||||
if shorten:
|
if shorten:
|
||||||
|
@ -35,11 +35,18 @@ class Module(ModuleManager.BaseModule):
|
||||||
link = self.exports.get("shorturl")(server, link)
|
link = self.exports.get("shorturl")(server, link)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
link = " - %s" % link if link else ""
|
link = "%s" % link if link else ""
|
||||||
|
|
||||||
feed_title_str = "%s: " % feed_title if feed_title else ""
|
feed_title_str = "%s" % feed_title if feed_title else ""
|
||||||
|
# just in case the format starts keyerroring and you're not sure why
|
||||||
|
self.log.trace("RSS Entry: " + str(entry))
|
||||||
|
try:
|
||||||
|
format = channel.get_setting("rss-format", "$longtitle: $title by $author - $link").replace("$longtitle", feed_title_str).replace("$title", title).replace("$link", link).replace("$author", author).format(**entry)
|
||||||
|
except KeyError:
|
||||||
|
self.log.warn(f"Failed to format RSS entry for {channel}. Falling back to default format.")
|
||||||
|
format = f"{feed_title_str}: {title} by {author} - {link}"
|
||||||
|
|
||||||
return "%s%s%s%s" % (feed_title_str, title, author, link)
|
return format
|
||||||
|
|
||||||
def _timer(self, timer):
|
def _timer(self, timer):
|
||||||
start_time = time.monotonic()
|
start_time = time.monotonic()
|
||||||
|
@ -106,7 +113,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
valid += 1
|
valid += 1
|
||||||
|
|
||||||
shorten = channel.get_setting("rss-shorten", False)
|
shorten = channel.get_setting("rss-shorten", False)
|
||||||
output = self._format_entry(server, feed_title, entry,
|
output = self._format_entry(server, channel, feed_title, entry,
|
||||||
shorten)
|
shorten)
|
||||||
|
|
||||||
self.events.on("send.stdout").call(target=channel,
|
self.events.on("send.stdout").call(target=channel,
|
||||||
|
@ -200,10 +207,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
|
|
||||||
title, entries = self._get_entries(url)
|
title, entries = self._get_entries(url)
|
||||||
if not entries:
|
if not entries:
|
||||||
raise utils.EventError("Failed to get RSS entries")
|
raise utils.EventError("%s has no entries" % url)
|
||||||
|
|
||||||
shorten = event["target"].get_setting("rss-shorten", False)
|
shorten = event["target"].get_setting("rss-shorten", False)
|
||||||
out = self._format_entry(event["server"], title, entries[0],
|
out = self._format_entry(event["server"], event["target"], title, entries[0],
|
||||||
shorten)
|
shorten)
|
||||||
event["stdout"].write(out)
|
event["stdout"].write(out)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import re, traceback
|
import re, traceback
|
||||||
from src import ModuleManager, utils
|
from src import ModuleManager, utils
|
||||||
|
|
||||||
REGEX_SED = re.compile("^(?:(\\S+)[:,] )?s/")
|
REGEX_SED = re.compile(r"^(?:(\S+)[:,] )?s([/,`#]).*\2")
|
||||||
|
|
||||||
@utils.export("channelset",
|
@utils.export("channelset",
|
||||||
utils.BoolSetting("sed","Disable/Enable sed in a channel"))
|
utils.BoolSetting("sed","Disable/Enable sed in a channel"))
|
||||||
|
@ -35,7 +35,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
sed.replace = utils.irc.bold(sed.replace)
|
sed.replace = utils.irc.bold(sed.replace)
|
||||||
|
|
||||||
if self._closest_setting(event, "sed-sender-only", False):
|
if self._closest_setting(event, "sed-sender-only", False):
|
||||||
for_user = event["user"].nickname
|
for_user = event["user"].nickname_lower
|
||||||
|
|
||||||
match_line = None
|
match_line = None
|
||||||
match_message = None
|
match_message = None
|
||||||
|
|
|
@ -41,7 +41,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
|
|
||||||
@utils.export("shorturl-any")
|
@utils.export("shorturl-any")
|
||||||
def _shorturl_any(self, url):
|
def _shorturl_any(self, url):
|
||||||
return self._call_shortener(server, None, "bitly", url) or url
|
return self._call_shortener(None, None, "bitly", url) or url
|
||||||
|
|
||||||
@utils.export("shorturl")
|
@utils.export("shorturl")
|
||||||
def _shorturl(self, server, url, context=None):
|
def _shorturl(self, server, url, context=None):
|
||||||
|
|
|
@ -2,7 +2,7 @@ import datetime, html, time
|
||||||
from src import utils
|
from src import utils
|
||||||
|
|
||||||
def _timestamp(dt):
|
def _timestamp(dt):
|
||||||
seconds_since = time.time()-dt.timestamp()
|
seconds_since = time.time()-dt.replace(tzinfo=datetime.timezone.utc).timestamp()
|
||||||
timestamp = utils.datetime.format.to_pretty_since(
|
timestamp = utils.datetime.format.to_pretty_since(
|
||||||
seconds_since, max_units=2)
|
seconds_since, max_units=2)
|
||||||
return "%s ago" % timestamp
|
return "%s ago" % timestamp
|
||||||
|
|
|
@ -69,6 +69,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
|
|
||||||
celsius = "%dC" % page["main"]["temp"]
|
celsius = "%dC" % page["main"]["temp"]
|
||||||
fahrenheit = "%dF" % ((page["main"]["temp"]*(9/5))+32)
|
fahrenheit = "%dF" % ((page["main"]["temp"]*(9/5))+32)
|
||||||
|
kelvin = "%dK" % ((page["main"]["temp"])+273.15)
|
||||||
description = page["weather"][0]["description"].title()
|
description = page["weather"][0]["description"].title()
|
||||||
humidity = "%s%%" % page["main"]["humidity"]
|
humidity = "%s%%" % page["main"]["humidity"]
|
||||||
|
|
||||||
|
@ -81,10 +82,11 @@ class Module(ModuleManager.BaseModule):
|
||||||
location_str = "(%s) %s" % (nickname, location_str)
|
location_str = "(%s) %s" % (nickname, location_str)
|
||||||
|
|
||||||
event["stdout"].write(
|
event["stdout"].write(
|
||||||
"%s | %s/%s | %s | Humidity: %s | Wind: %s/%s" % (
|
"%s | %s/%s/%s | %s | Humidity: %s | Wind: %s/%s" % (
|
||||||
location_str, celsius, fahrenheit, description, humidity,
|
location_str, celsius, fahrenheit, kelvin, description,
|
||||||
wind_speed_k, wind_speed_m))
|
humidity, wind_speed_k, wind_speed_m))
|
||||||
else:
|
else:
|
||||||
event["stderr"].write("No weather information for this location")
|
event["stderr"].write("No weather information for this location")
|
||||||
else:
|
else:
|
||||||
raise utils.EventResultsError()
|
raise utils.EventResultsError()
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ PySocks ==1.7.1
|
||||||
python-dateutil ==2.8.1
|
python-dateutil ==2.8.1
|
||||||
pytz ==2019.2
|
pytz ==2019.2
|
||||||
requests ==2.22.0
|
requests ==2.22.0
|
||||||
scrypt ==0.8.13
|
|
||||||
suds-jurko ==0.6
|
suds-jurko ==0.6
|
||||||
tornado ==6.0.3
|
tornado ==6.0.3
|
||||||
tweepy ==3.8.0
|
tweepy ==3.8.0
|
||||||
|
|
|
@ -116,7 +116,8 @@ class Bot(object):
|
||||||
self._trigger_both()
|
self._trigger_both()
|
||||||
return returned
|
return returned
|
||||||
|
|
||||||
func_queue = queue.Queue(1) # type: queue.Queue[str]
|
func_queue: queue.Queue[typing.Tuple[TriggerResult, typing.Any]
|
||||||
|
] = queue.Queue(1)
|
||||||
|
|
||||||
def _action():
|
def _action():
|
||||||
try:
|
try:
|
||||||
|
@ -134,7 +135,8 @@ class Bot(object):
|
||||||
if trigger_threads:
|
if trigger_threads:
|
||||||
self._trigger_both()
|
self._trigger_both()
|
||||||
|
|
||||||
if type == TriggerResult.Exception:
|
if (type == TriggerResult.Exception and
|
||||||
|
isinstance(returned, Exception)):
|
||||||
raise returned
|
raise returned
|
||||||
elif type == TriggerResult.Return:
|
elif type == TriggerResult.Return:
|
||||||
return returned
|
return returned
|
||||||
|
|
|
@ -95,8 +95,12 @@ class Server(IRCObject.Object):
|
||||||
self.connection_params.bindhost,
|
self.connection_params.bindhost,
|
||||||
self.connection_params.tls,
|
self.connection_params.tls,
|
||||||
tls_verify=self.get_setting("ssl-verify", True),
|
tls_verify=self.get_setting("ssl-verify", True),
|
||||||
cert=self.bot.config.get("tls-certificate", None),
|
cert=self.bot.config.get("tls-certificate", '').format(
|
||||||
key=self.bot.config.get("tls-key", None))
|
DATA=self.bot.data_directory
|
||||||
|
) or None,
|
||||||
|
key=self.bot.config.get("tls-key", '').format(
|
||||||
|
DATA=self.bot.data_directory
|
||||||
|
))
|
||||||
self.events.on("preprocess.connect").call(server=self)
|
self.events.on("preprocess.connect").call(server=self)
|
||||||
self.socket.connect()
|
self.socket.connect()
|
||||||
|
|
||||||
|
|
|
@ -151,6 +151,27 @@ class Module(ModuleManager.BaseModule):
|
||||||
return
|
return
|
||||||
event["stdout"].write("Added server '%s'" % alias)
|
event["stdout"].write("Added server '%s'" % alias)
|
||||||
|
|
||||||
|
@utils.hook("received.command.delserver")
|
||||||
|
@utils.kwarg("help", "Delete a server")
|
||||||
|
@utils.kwarg("pemission", "delserver")
|
||||||
|
@utils.spec("!<alias>word")
|
||||||
|
def del_server(self, event):
|
||||||
|
alias = event["spec"][0]
|
||||||
|
sid = self.bot.database.servers.by_alias(alias)
|
||||||
|
if sid == None:
|
||||||
|
event["stderr"].write("Server '%s' does not exist" % alias)
|
||||||
|
return
|
||||||
|
if self._server_from_alias(alias):
|
||||||
|
event["stderr"].write("You must disconnect from %s before deleting it" % alias)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.bot.database.servers.delete(sid)
|
||||||
|
except Exception as e:
|
||||||
|
event["stderr"].write("Failed to delete server")
|
||||||
|
self.log.error("failed to add server \"%s\"", [alias], exc_info=True)
|
||||||
|
return
|
||||||
|
event["stderr"].write("Server '%s' has been deleted" % alias)
|
||||||
|
|
||||||
@utils.hook("received.command.editserver")
|
@utils.hook("received.command.editserver")
|
||||||
@utils.kwarg("help", "Edit server details")
|
@utils.kwarg("help", "Edit server details")
|
||||||
@utils.kwarg("permission", "editserver")
|
@utils.kwarg("permission", "editserver")
|
||||||
|
|
|
@ -5,15 +5,15 @@ def bool_input(s: str):
|
||||||
return not result or result[0].lower() in ["", "y"]
|
return not result or result[0].lower() in ["", "y"]
|
||||||
|
|
||||||
def add_server():
|
def add_server():
|
||||||
alias = input("alias: ")
|
alias = input("alias (display name): ")
|
||||||
hostname = input("hostname: ")
|
hostname = input("hostname (address of server): ")
|
||||||
port = int(input("port: "))
|
port = int(input("port: "))
|
||||||
tls = bool_input("tls?")
|
tls = bool_input("tls?")
|
||||||
password = input("password?: ")
|
password = input("password (optional, leave blank to skip): ")
|
||||||
nickname = input("nickname: ")
|
nickname = input("nickname: ")
|
||||||
username = input("username: ")
|
username = input("username (optional): ")
|
||||||
realname = input("realname: ")
|
realname = input("realname (optional): ")
|
||||||
bindhost = input("bindhost?: ")
|
bindhost = input("bindhost (optional): ")
|
||||||
|
|
||||||
return irc.IRCConnectionParameters(-1, alias, hostname, port, password, tls,
|
return irc.IRCConnectionParameters(-1, alias, hostname, port, password, tls,
|
||||||
bindhost, nickname, username, realname)
|
bindhost, nickname, username, realname)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from requests_toolbelt.adapters import source
|
||||||
|
|
||||||
REGEX_URL = re.compile("https?://\S+", re.I)
|
REGEX_URL = re.compile("https?://\S+", re.I)
|
||||||
|
|
||||||
PAIRED_CHARACTERS = ["<>", "()"]
|
PAIRED_CHARACTERS = [("<", ">"), ("(", ")")]
|
||||||
|
|
||||||
# best-effort tidying up of URLs
|
# best-effort tidying up of URLs
|
||||||
def url_sanitise(url: str):
|
def url_sanitise(url: str):
|
||||||
|
|
|
@ -44,7 +44,7 @@ class SedMatch(Sed):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _sed_split(s: str) -> typing.List[str]:
|
def _sed_split(s: str) -> typing.List[str]:
|
||||||
tokens = _tokens(s, "/")
|
tokens = _tokens(s, s[1])
|
||||||
if tokens and (not tokens[-1] == (len(s)-1)):
|
if tokens and (not tokens[-1] == (len(s)-1)):
|
||||||
tokens.append(len(s))
|
tokens.append(len(s))
|
||||||
|
|
||||||
|
|
|
@ -25,13 +25,20 @@ def ssl_wrap(sock: socket.socket, cert: str=None, key: str=None,
|
||||||
def constant_time_compare(a: typing.AnyStr, b: typing.AnyStr) -> bool:
|
def constant_time_compare(a: typing.AnyStr, b: typing.AnyStr) -> bool:
|
||||||
return hmac.compare_digest(a, b)
|
return hmac.compare_digest(a, b)
|
||||||
|
|
||||||
import scrypt
|
import hashlib
|
||||||
def password(byte_n: int=32) -> str:
|
def password(byte_n: int=32) -> str:
|
||||||
return binascii.hexlify(os.urandom(byte_n)).decode("utf8")
|
return binascii.hexlify(os.urandom(byte_n)).decode("utf8")
|
||||||
def salt(byte_n: int=64) -> str:
|
def salt(byte_n: int=64) -> str:
|
||||||
return base64.b64encode(os.urandom(byte_n)).decode("utf8")
|
return base64.b64encode(os.urandom(byte_n)).decode("utf8")
|
||||||
def hash(given_salt: str, data: str):
|
def hash(given_salt: str, data: str):
|
||||||
return base64.b64encode(scrypt.hash(data, given_salt)).decode("utf8")
|
hash = hashlib.scrypt(
|
||||||
|
data.encode("utf8"),
|
||||||
|
salt=given_salt.encode("utf8"),
|
||||||
|
n=1<<14,
|
||||||
|
r=8,
|
||||||
|
p=1
|
||||||
|
)
|
||||||
|
return base64.b64encode(hash).decode("ascii")
|
||||||
def hash_verify(salt: str, data: str, compare: str):
|
def hash_verify(salt: str, data: str, compare: str):
|
||||||
given_hash = hash(salt, data)
|
given_hash = hash(salt, data)
|
||||||
return constant_time_compare(given_hash, compare)
|
return constant_time_compare(given_hash, compare)
|
||||||
|
|
Loading…
Reference in a new issue