Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions beryllia/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from .util import looks_like_glob, colourise

from .parse.nickserv import NickServParser
from .parse.chanserv import ChanServParser
from .parse.operserv import OperServParser
from .parse.snote import SnoteParser

RE_DATE = re_compile(r"^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})$")
Expand Down Expand Up @@ -131,6 +133,8 @@ async def line_read(self, line: Line):
self._database_init = True

self._nickserv = NickServParser(database)
self._chanserv = ChanServParser(database)
self._operserv = OperServParser(database)
self._snote = SnoteParser(database, self._config.rejects, self._kline_new)

elif line.command == RPL_YOUREOPER:
Expand Down Expand Up @@ -166,6 +170,14 @@ async def _on_message(self, line: Line) -> None:
await self._nickserv.handle(line)
return

if line.hostmask.nickname == "ChanServ":
await self._chanserv.handle(line)
return

if line.hostmask.nickname == "OperServ":
await self._operserv.handle(line)
return

first, _, rest = line.params[1].partition(" ")
if self.is_me(line.params[0]):
# private message
Expand Down
8 changes: 8 additions & 0 deletions beryllia/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
from .email_resolve import EmailResolveTable
from .account_freeze import AccountFreezeTable
from .freeze_tag import FreezeTagTable
from .channel_close import ChannelCloseTable
from .close_tag import CloseTagTable
from .klinechan import KLineChanTable
from .klinechan_tag import KLineChanTagTable

from ..normalise import SearchNormaliser

Expand All @@ -41,6 +45,10 @@ def __init__(self, pool: asyncpg.Pool, normaliser: SearchNormaliser):
self.email_resolve = EmailResolveTable(pool, normaliser)
self.account_freeze = AccountFreezeTable(pool, normaliser)
self.freeze_tag = FreezeTagTable(pool, normaliser)
self.channel_close = ChannelCloseTable(pool, normaliser)
self.close_tag = CloseTagTable(pool, normaliser)
self.klinechan = KLineChanTable(pool, normaliser)
self.klinechan_tag = KLineChanTagTable(pool, normaliser)

@staticmethod
async def connect(
Expand Down
12 changes: 12 additions & 0 deletions beryllia/database/channel_close.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from .common import Table

class ChannelCloseTable(Table):
async def add(self, channel: str, soper: str, reason: str) -> int:
query = """
INSERT INTO channel_close (channel, soper, reason, ts)
VALUES ($1, $2, $3, NOW()::TIMESTAMP)
RETURNING id
"""

async with self.pool.acquire() as conn:
return await conn.fetchval(query, channel, soper, reason)
30 changes: 30 additions & 0 deletions beryllia/database/close_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from .common import Table
from ..normalise import SearchType


class CloseTagTable(Table):
async def add(self, close_id: int, tag: str, soper: str) -> None:

query = """
INSERT INTO close_tag (close_id, tag, search_tag, soper, ts)
VALUES ($1, $2, $3, $4, NOW()::TIMESTAMP)
"""

async with self.pool.acquire() as conn:
await conn.execute(
query, close_id, tag, str(self.to_search(tag, SearchType.TAG)), soper
)

async def exists(self, close_id: int, tag: str) -> bool:
query = """
SELECT 1 FROM close_tag
WHERE close_id = $1
AND search_tag = $2
"""

async with self.pool.acquire() as conn:
return bool(
await conn.fetchval(
query, close_id, str(self.to_search(tag, SearchType.TAG))
)
)
12 changes: 12 additions & 0 deletions beryllia/database/klinechan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from .common import Table

class KLineChanTable(Table):
async def add(self, channel: str, soper: str, reason: str) -> int:
query = """
INSERT INTO klinechan (channel, soper, reason, ts)
VALUES ($1, $2, $3, NOW()::TIMESTAMP)
RETURNING id
"""

async with self.pool.acquire() as conn:
return await conn.fetchval(query, channel, soper, reason)
28 changes: 28 additions & 0 deletions beryllia/database/klinechan_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from .common import Table
from ..normalise import SearchType

class KLineChanTagTable(Table):
async def add(self, klinechan_id: int, tag: str, soper: str) -> None:
query = """
INSERT INTO klinechan_tag (klinechan_id, tag, search_tag, soper, ts)
VALUES ($1, $2, $3, $4, NOW()::TIMESTAMP)
"""

async with self.pool.acquire() as conn:
await conn.execute(
query, klinechan_id, tag, str(self.to_search(tag, SearchType.TAG)), soper
)

async def exists(self, klinechan_id: int, tag: str) -> bool:
query = """
SELECT 1 FROM klinechan_tag
WHERE klinechan_id = $1
AND search_tag = $2
"""

async with self.pool.acquire() as conn:
return bool(
await conn.fetchval(
query, klinechan_id, str(self.to_search(tag, SearchType.TAG))
)
)
73 changes: 73 additions & 0 deletions beryllia/parse/chanserv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from re import compile as re_compile, search as re_search
from typing import Any, Awaitable, Callable, Dict, List, Optional

from irctokens import Line

from .common import IRCParser, RE_EMBEDDEDTAG
from ..database import Database

RE_COMMAND = re_compile(
r"^(?P<nickname>\S+)"
# this is optional in command output
r"( (?P<account>[(]\S*[)]))?"
r" (?P<command>\S+):"
r"( (?P<args>.*))?$"
)

_TYPE_HANDLER = Callable[[Any, str, Optional[str], str], Awaitable[None]]
_HANDLERS: Dict[str, _TYPE_HANDLER] = {}


def _handler(command: str) -> Callable[[_TYPE_HANDLER], _TYPE_HANDLER]:
def _inner(func: _TYPE_HANDLER) -> _TYPE_HANDLER:
_HANDLERS[command] = func
return func

return _inner


class ChanServParser(IRCParser):
def __init__(self, database: Database):
super().__init__()
self._database = database

async def handle(self, line: Line) -> None:
message = line.params[1]
match = RE_COMMAND.search(message)
if match is None:
return

command = match.group("command")
if not command in _HANDLERS:
return

nickname = match.group("nickname")
account = match.group("account")
args = match.group("args")

func = _HANDLERS[command]
await func(self, nickname, account, args)

@_handler("CLOSE:ON")
async def _handle_CLOSE_ON(
self, soper: str, soper_account: Optional[str], args: str
) -> None:

match = re_search(r"(?P<channel>\S+) \(reason: (?P<reason>.*)\)$", args)
if match is None:
return

soper = soper_account or soper

channel = match.group("channel")
reason = match.group("reason")

close_id = await self._database.channel_close.add(channel, soper, reason)

tags = list(RE_EMBEDDEDTAG.finditer(reason))
for tag_match in tags:
tag = tag_match.group(1)
if await self._database.close_tag.exists(close_id, tag):
continue

await self._database.close_tag.add(close_id, tag, soper)
73 changes: 73 additions & 0 deletions beryllia/parse/operserv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from re import compile as re_compile, search as re_search
from typing import Any, Awaitable, Callable, Dict, List, Optional

from irctokens import Line

from .common import IRCParser, RE_EMBEDDEDTAG
from ..database import Database

RE_COMMAND = re_compile(
r"^(?P<nickname>\S+)"
# this is optional in command output
r"( (?P<account>[(]\S*[)]))?"
r" (?P<command>\S+):"
r"( (?P<args>.*))?$"
)

_TYPE_HANDLER = Callable[[Any, str, Optional[str], str], Awaitable[None]]
_HANDLERS: Dict[str, _TYPE_HANDLER] = {}


def _handler(command: str) -> Callable[[_TYPE_HANDLER], _TYPE_HANDLER]:
def _inner(func: _TYPE_HANDLER) -> _TYPE_HANDLER:
_HANDLERS[command] = func
return func

return _inner


class OperServParser(IRCParser):
def __init__(self, database: Database):
super().__init__()
self._database = database

async def handle(self, line: Line) -> None:
message = line.params[1]
match = RE_COMMAND.search(message)
if match is None:
return

command = match.group("command")
if not command in _HANDLERS:
return

nickname = match.group("nickname")
account = match.group("account")
args = match.group("args")

func = _HANDLERS[command]
await func(self, nickname, account, args)

@_handler("KLINECHAN:ON")
async def _handle_KLINECHAN_ON(
self, soper: str, soper_account: Optional[str], args: str
) -> None:

match = re_search(r"(?P<channel>\S+) \(reason: (?P<reason>.*)\)$", args)
if match is None:
return

soper = soper_account or soper

channel = match.group("channel")
reason = match.group("reason")

close_id = await self._database.klinechan.add(channel, soper, reason)

tags = list(RE_EMBEDDEDTAG.finditer(reason))
for tag_match in tags:
tag = tag_match.group(1)
if await self._database.klinechan_tag.exists(close_id, tag):
continue

await self._database.klinechan_tag.add(close_id, tag, soper)
35 changes: 35 additions & 0 deletions make-database.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
-- 10 is username length
-- 32 is kline tag length
-- 50 is realname length
-- 50 is channel length
-- 64 is hostname length
-- 92 is mask length
-- 260 is reason length
Expand Down Expand Up @@ -175,6 +176,40 @@ CREATE TABLE freeze_tag (
PRIMARY KEY (freeze_id, search_tag)
);

CREATE TABLE channel_close (
id SERIAL PRIMARY KEY,
channel VARCHAR(50) NOT NULL,
soper VARCHAR(16) NOT NULL,
reason VARCHAR(256) NOT NULL,
ts TIMESTAMP NOT NULL
);

CREATE TABLE close_tag (
close_id INTEGER NOT NULL REFERENCES channel_close (id) ON DELETE CASCADE,
tag VARCHAR(32) NOT NULL,
search_tag VARCHAR(32) NOT NULL,
soper VARCHAR(16) NOT NULL,
ts TIMESTAMP NOT NULL,
PRIMARY KEY (close_id, search_tag)
);

CREATE TABLE klinechan (
id SERIAL PRIMARY KEY,
channel VARCHAR(50) NOT NULL,
soper VARCHAR(16) NOT NULL,
reason VARCHAR(256) NOT NULL,
ts TIMESTAMP NOT NULL
);

CREATE TABLE klinechan_tag (
klinechan_id INTEGER NOT NULL REFERENCES klinechan (id) ON DELETE CASCADE,
tag VARCHAR(32) NOT NULL,
search_tag VARCHAR(32) NOT NULL,
soper VARCHAR(16) NOT NULL,
ts TIMESTAMP NOT NULL,
PRIMARY KEY (klinechan_id, search_tag)
);

CREATE TABLE statsp (
oper VARCHAR(16) NOT NULL,
mask VARCHAR(92) NOT NULL,
Expand Down