wikilink with semantic syntax :)
This commit is contained in:
parent
4f239ea0d7
commit
01b1860e4d
10 changed files with 1188 additions and 574 deletions
42
README.md
42
README.md
|
@ -4,3 +4,45 @@ Bot to add tweets using an extended wikilink syntax to the wiki
|
||||||
|
|
||||||
Starting with twitter, but then will add masto
|
Starting with twitter, but then will add masto
|
||||||
|
|
||||||
|
# Mediawiki
|
||||||
|
|
||||||
|
https://www.mediawiki.org/wiki/Manual:Creating_a_bot
|
||||||
|
|
||||||
|
- Go to `Special:BotPasswords`
|
||||||
|
- Permissions
|
||||||
|
- Edit existing pages
|
||||||
|
- Create, edit, and move pages
|
||||||
|
-
|
||||||
|
- save to `mediawiki_creds.json` like
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user": "Jonny@wikibot",
|
||||||
|
"password": "<THE BOT PASSWORD>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Slack
|
||||||
|
|
||||||
|
Using the `python-slack-sdk` https://slack.dev/python-slack-sdk/
|
||||||
|
|
||||||
|
## Make App
|
||||||
|
|
||||||
|
https://slack.dev/python-slack-sdk/socket-mode/index.html
|
||||||
|
|
||||||
|
- Make slack app - https://api.slack.com/apis/connections/socket#setup
|
||||||
|
- Assign to workplace
|
||||||
|
- Permissions
|
||||||
|
- `channels:history`
|
||||||
|
- `channels:read`
|
||||||
|
- `app_mentions:read`
|
||||||
|
- `chat:write`
|
||||||
|
- `links:read`
|
||||||
|
- `reactions:read`
|
||||||
|
- `reactions:write`
|
||||||
|
- `users:read`
|
||||||
|
- Create App-level token with `connections:write`
|
||||||
|
- Configure app
|
||||||
|
- Enable socket mode - https://api.slack.com/apis/connections/socket#toggling
|
||||||
|
- Enable Events - `message.channels`
|
||||||
|
-
|
1238
poetry.lock
generated
1238
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -10,8 +10,10 @@ packages = [
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
wikipostbot = "wiki_postbot.main:main"
|
wikipostbot = "wiki_postbot.main:main"
|
||||||
discord_bot = "wiki_postbot.clients.discord_client:main"
|
discord_bot = "wiki_postbot.clients.discord:main"
|
||||||
install_discord_bot = "wiki_postbot.service:main"
|
install_discord_bot = "wiki_postbot.service:main"
|
||||||
|
slack_bot = "wiki_postbot.clients.slack:main"
|
||||||
|
install_slack_bot = "wiki_postbot.service:main_slack"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.9"
|
python = "^3.9"
|
||||||
|
@ -20,10 +22,21 @@ rich = "^12.4.4"
|
||||||
parse = "^1.19.0"
|
parse = "^1.19.0"
|
||||||
pywikibot = "^7.7.0"
|
pywikibot = "^7.7.0"
|
||||||
pyparsing = "^3.0.9"
|
pyparsing = "^3.0.9"
|
||||||
"discord.py" = "^2.0.1"
|
|
||||||
wikitextparser = "^0.51.1"
|
wikitextparser = "^0.51.1"
|
||||||
certifi = "^2022.9.24"
|
certifi = "^2022.9.24"
|
||||||
|
|
||||||
|
[tool.poetry.group.discord]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[tool.poetry.group.discord.dependencies]
|
||||||
|
"discord.py" = "^2.0.1"
|
||||||
|
|
||||||
|
[tool.poetry.group.slack]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[tool.poetry.group.slack.dependencies]
|
||||||
|
slack-sdk = "^3.21.3"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^7.1.2"
|
pytest = "^7.1.2"
|
||||||
Faker = "^15.1.0"
|
Faker = "^15.1.0"
|
||||||
|
|
91
wiki_postbot/clients/client.py
Normal file
91
wiki_postbot/clients/client.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from wiki_postbot.interfaces.mediawiki import Wiki
|
||||||
|
from wiki_postbot.logger import init_logger
|
||||||
|
from wiki_postbot.patterns.wikilink import Wikilink
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from logging import Logger
|
||||||
|
|
||||||
|
class Client(ABC):
|
||||||
|
"""
|
||||||
|
Metaclass for different clients (things that receive inputs like discord/slack messages)
|
||||||
|
pending some actual formalization of the concepts in this library (like idk what do we
|
||||||
|
call the wiki, a sink? idk)
|
||||||
|
|
||||||
|
Subclasses must implement all the abstract methods (that is, i guess, the definition
|
||||||
|
of abstract methods) but may do whatever other logic they need to do to handle messages,
|
||||||
|
eg. discord bot subclasses
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
wiki:Wiki,
|
||||||
|
name: str = "wikibot_client",
|
||||||
|
log_dir: Path = Path('/var/log/wikibot'),
|
||||||
|
):
|
||||||
|
super(Client, self).__init__()
|
||||||
|
|
||||||
|
self._wiki = None
|
||||||
|
self.wiki = wiki
|
||||||
|
self.name = name
|
||||||
|
self.log_dir = Path(log_dir)
|
||||||
|
|
||||||
|
self.logger = self._init_logger(name=self.name, log_dir=self.log_dir)
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# abstract methods
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
# @abstractmethod
|
||||||
|
# def _react_progress(self, message):
|
||||||
|
# """React to a message indicating that we are processing it"""
|
||||||
|
# pass
|
||||||
|
|
||||||
|
# @abstractmethod
|
||||||
|
# def _react_complete(self, message):
|
||||||
|
# """React to a message indicating that we have completed processing it"""
|
||||||
|
# pass
|
||||||
|
# @abstractmethod
|
||||||
|
# def _react_error(self, message):
|
||||||
|
# """React to a message indicating there was some error"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def parse_wikilinks(self, message) -> List[Wikilink]:
|
||||||
|
"""Parse a given message, returning the wikilinks it contains"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def handle_wikilinks(self, message, wl: List[Wikilink]):
|
||||||
|
"""Handle the message with wikilinks, most likely by posting it to the wiki!"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Properties
|
||||||
|
# --------------------------------------------------
|
||||||
|
@property
|
||||||
|
def wiki(self) -> Wiki:
|
||||||
|
return self._wiki
|
||||||
|
|
||||||
|
@wiki.setter
|
||||||
|
def wiki(self, wiki: Wiki):
|
||||||
|
if wiki.sess is None:
|
||||||
|
raise RuntimeError("Wiki client is not logged in! Login before passing to client")
|
||||||
|
self._wiki = wiki
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Private methods
|
||||||
|
# --------------------------------------------------
|
||||||
|
def _init_logger(self, name:str, log_dir:Path) -> 'Logger':
|
||||||
|
|
||||||
|
# Try and make log directory, if we cant, it should fail.
|
||||||
|
log_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
logger = init_logger(name=name, basedir=log_dir)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,42 +8,40 @@ from wiki_postbot.creds import Discord_Creds, Mediawiki_Creds
|
||||||
from wiki_postbot.patterns.wikilink import Wikilink
|
from wiki_postbot.patterns.wikilink import Wikilink
|
||||||
from wiki_postbot.interfaces.mediawiki import Wiki
|
from wiki_postbot.interfaces.mediawiki import Wiki
|
||||||
from wiki_postbot.logger import init_logger
|
from wiki_postbot.logger import init_logger
|
||||||
|
from wiki_postbot.clients.client import Client as Wikibot_Client
|
||||||
|
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord import Emoji
|
from discord import Emoji
|
||||||
import pdb
|
import pdb
|
||||||
|
|
||||||
class DiscordClient(Client):
|
class DiscordClient(Client, Wikibot_Client):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
wiki:Wiki,
|
wiki:Wiki,
|
||||||
|
name:str='discord_bot',
|
||||||
intents=None,
|
intents=None,
|
||||||
debug:bool=False,
|
debug:bool=False,
|
||||||
reply_channel:str="wikibot",
|
reply_channel:str="wikibot",
|
||||||
log_dir:Path=Path('/var/log/wikibot'),
|
log_dir:Path=Path('/var/log/wikibot'),
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
|
Wikibot_Client.__init__(
|
||||||
|
self,
|
||||||
|
wiki=wiki,
|
||||||
|
name=name,
|
||||||
|
log_dir=log_dir)
|
||||||
|
|
||||||
if intents is None:
|
if intents is None:
|
||||||
intents = Intents.default()
|
intents = Intents.default()
|
||||||
intents.message_content = True
|
intents.message_content = True
|
||||||
|
|
||||||
self.wiki = wiki
|
Client.__init__(self, intents=intents, **kwargs)
|
||||||
if self.wiki.sess is None:
|
|
||||||
raise RuntimeError("Wiki client is not logged in! Login before passing to discord client")
|
|
||||||
|
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.reply_channel_name = reply_channel
|
self.reply_channel_name = reply_channel
|
||||||
self.reply_channel = None # type: Optional[discord.TextChannel]
|
self.reply_channel = None # type: Optional[discord.TextChannel]
|
||||||
|
|
||||||
self.log_dir = Path(log_dir)
|
|
||||||
# Try and make log directory, if we cant, it should fail.
|
|
||||||
self.log_dir.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
self.logger = init_logger(name="discord_bot", basedir=self.log_dir)
|
|
||||||
|
|
||||||
super(DiscordClient, self).__init__(intents=intents, **kwargs)
|
|
||||||
|
|
||||||
async def get_channel(self, channel_name:str) -> discord.TextChannel:
|
async def get_channel(self, channel_name:str) -> discord.TextChannel:
|
||||||
channel = discord.utils.get(self.get_all_channels(), name=channel_name)
|
channel = discord.utils.get(self.get_all_channels(), name=channel_name)
|
||||||
self.logger.debug(f"Got channel {channel}")
|
self.logger.debug(f"Got channel {channel}")
|
||||||
|
@ -69,15 +67,15 @@ class DiscordClient(Client):
|
||||||
await message.add_reaction("❤️🔥")
|
await message.add_reaction("❤️🔥")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
wl = Wikilink.parse(message.content)
|
wl = self.parse_wikilinks(message)
|
||||||
self.logger.debug(f"Parsed wikilinks: {wl}")
|
self.logger.debug(f"Parsed wikilinks: {wl}")
|
||||||
if len(wl)>0:
|
if len(wl)>0:
|
||||||
await self.handle_wikilink(message, wl)
|
await self.handle_wikilinks(message, wl)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(f"Error parsing wikilink! got exception: ")
|
self.logger.exception(f"Error parsing wikilink! got exception: ")
|
||||||
|
|
||||||
|
|
||||||
async def handle_wikilink(self, message:discord.message.Message, wl:List[Wikilink]):
|
async def handle_wikilinks(self, message:discord.message.Message, wl:List[Wikilink]):
|
||||||
log_msg = f"Wikilinks detected: \n" + '\n'.join([str(l) for l in wl])
|
log_msg = f"Wikilinks detected: \n" + '\n'.join([str(l) for l in wl])
|
||||||
self.logger.info(log_msg)
|
self.logger.info(log_msg)
|
||||||
if self.debug:
|
if self.debug:
|
||||||
|
@ -109,6 +107,10 @@ class DiscordClient(Client):
|
||||||
|
|
||||||
# TODO: Logging!
|
# TODO: Logging!
|
||||||
|
|
||||||
|
def parse_wikilinks(self, message) -> List[Wikilink]:
|
||||||
|
wikilinks = Wikilink.parse(message.content)
|
||||||
|
return wikilinks
|
||||||
|
|
||||||
# def add_links(self, links:Wikilink, msg:discord.message.Message):
|
# def add_links(self, links:Wikilink, msg:discord.message.Message):
|
||||||
# if 'testing links' in message.content:
|
# if 'testing links' in message.content:
|
||||||
# await message.channel.send(embed=Embed().add_field(name="Links", value="there are [links](https://example.com)"))
|
# await message.channel.send(embed=Embed().add_field(name="Links", value="there are [links](https://example.com)"))
|
264
wiki_postbot/clients/slack.py
Normal file
264
wiki_postbot/clients/slack.py
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
import pdb
|
||||||
|
from pprint import pformat
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Tuple, Union, Dict
|
||||||
|
from threading import Event
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from wiki_postbot.clients.client import Client
|
||||||
|
from wiki_postbot.creds import Mediawiki_Creds, Slack_Creds
|
||||||
|
from wiki_postbot.interfaces.mediawiki import Wiki
|
||||||
|
from wiki_postbot.patterns.wikilink import Wikilink
|
||||||
|
|
||||||
|
from slack_sdk.web import WebClient
|
||||||
|
from slack_sdk.socket_mode import SocketModeClient
|
||||||
|
from slack_sdk.socket_mode.response import SocketModeResponse
|
||||||
|
from slack_sdk.socket_mode.request import SocketModeRequest
|
||||||
|
|
||||||
|
|
||||||
|
class SlackClient(Client):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
creds: Slack_Creds,
|
||||||
|
wiki:Wiki,
|
||||||
|
name:str="wikibot_slack",
|
||||||
|
reply_channel="wikibot",
|
||||||
|
log_dir:Path = Path('/var/log/wikibot'),
|
||||||
|
):
|
||||||
|
"""Wikibot but for slack!"""
|
||||||
|
super(SlackClient, self).__init__(wiki=wiki, name=name, log_dir=log_dir)
|
||||||
|
|
||||||
|
self.creds = creds
|
||||||
|
self._initialized = False
|
||||||
|
self.web_client = None # type: Optional[WebClient]
|
||||||
|
self.socket_client = None # type: Optional[SocketModeClient]
|
||||||
|
self.reply_channel_name = reply_channel
|
||||||
|
self._reply_channel = None
|
||||||
|
self._channel_inverse = None
|
||||||
|
# self.web_client, self.socket_client = self._init_client(self.creds)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reply_channel(self) -> Union[str, None]:
|
||||||
|
if self._reply_channel is None:
|
||||||
|
self.logger.debug(f"Getting channel named {self.reply_channel_name}")
|
||||||
|
channels = self.web_client.conversations_list()
|
||||||
|
channel = [c for c in channels['channels'] if c['name'] == self.reply_channel_name]
|
||||||
|
# self.logger.debug(channel)
|
||||||
|
if len(channel) == 1:
|
||||||
|
self._reply_channel = channel[0]['id']
|
||||||
|
elif len(channel) > 1:
|
||||||
|
self.logger.exception(f"Got too many channels to reply to!")
|
||||||
|
self._reply_channel = None
|
||||||
|
else:
|
||||||
|
self.logger("Reply channel not found!")
|
||||||
|
self._reply_channel = None
|
||||||
|
return self._reply_channel
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel_inverse(self) -> Dict[str,str]:
|
||||||
|
"""Maps channel IDs to channel names"""
|
||||||
|
if self._channel_inverse is None:
|
||||||
|
self.logger.debug("Getting inverse channel map")
|
||||||
|
channels = self.web_client.conversations_list()
|
||||||
|
self._channel_inverse = {c['id']: c['name'] for c in channels['channels']}
|
||||||
|
return self._channel_inverse
|
||||||
|
|
||||||
|
|
||||||
|
def _init_client(self, creds: Slack_Creds) -> Tuple[WebClient, SocketModeClient]:
|
||||||
|
web_client = WebClient(creds.bot_token)
|
||||||
|
socket_client = SocketModeClient(app_token=creds.app_token, web_client=web_client)
|
||||||
|
return web_client, socket_client
|
||||||
|
|
||||||
|
def handle_event(self, client:SocketModeClient, req: SocketModeRequest):
|
||||||
|
self.logger.debug(f"type: {req.type}, payload_type: {req.payload['type']}\n{pformat(req.payload)}")
|
||||||
|
|
||||||
|
if req.type == "events_api":
|
||||||
|
# acknowledge we got it so it don't get resent
|
||||||
|
response = SocketModeResponse(envelope_id=req.envelope_id)
|
||||||
|
client.send_socket_mode_response(response)
|
||||||
|
else:
|
||||||
|
self.logger.debug(f'Unhandled event type: {req.type}')
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if req.type == "events_api" and \
|
||||||
|
req.payload['type'] == 'event_callback' and \
|
||||||
|
req.payload['event'].get('type', False) == 'message':
|
||||||
|
# Handle messages
|
||||||
|
self.logger.debug(f"Handling message")
|
||||||
|
|
||||||
|
message_text = req.payload['event']['text']
|
||||||
|
|
||||||
|
if 'good bot' in message_text.lower():
|
||||||
|
self.good_bot(client, req)
|
||||||
|
|
||||||
|
try:
|
||||||
|
wl = self.parse_wikilinks(message_text)
|
||||||
|
if len(wl) > 0:
|
||||||
|
self.logger.debug(f"Parsed wikilinks: {wl}")
|
||||||
|
self.handle_wikilinks(req, wl, client)
|
||||||
|
else:
|
||||||
|
self.logger.debug("No wikilinks found")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("Error parsing wikilinks! got exception...")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"Was event, but not a message. Payload type: {req.payload['type']}, Event type: {req.payload.get('event', {}).get('type', 'unknown')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# pdb.set_trace()
|
||||||
|
# if
|
||||||
|
|
||||||
|
def parse_wikilinks(self, message) -> List[Wikilink]:
|
||||||
|
wikilinks = Wikilink.parse(message)
|
||||||
|
return wikilinks
|
||||||
|
|
||||||
|
def handle_wikilinks(self, message, wl: List[Wikilink], client:SocketModeClient):
|
||||||
|
self.react('hourglass', client, message)
|
||||||
|
try:
|
||||||
|
# expand fields in message
|
||||||
|
channel_id = message.payload['event']['channel']
|
||||||
|
channel_name = self.channel_inverse[channel_id]
|
||||||
|
msg = SlackMessage(
|
||||||
|
content = message.payload['event']['text'],
|
||||||
|
user_id = message.payload['event']['user'],
|
||||||
|
channel_id=channel_id,
|
||||||
|
channel = channel_name,
|
||||||
|
timestamp = message.payload['event']['ts']
|
||||||
|
)
|
||||||
|
msg.complete(client.web_client)
|
||||||
|
self.logger.debug(f"Posting message:\n{msg}")
|
||||||
|
result = self.wiki.handle_slack(msg)
|
||||||
|
ok = result.ok
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("Error handling slack message")
|
||||||
|
result = None
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
self.react('hourglass', client, message, remove=True)
|
||||||
|
if ok:
|
||||||
|
self.react('white_check_mark', client, message)
|
||||||
|
else:
|
||||||
|
self.react('x', client, message)
|
||||||
|
|
||||||
|
if result and result.reply:
|
||||||
|
if self.reply_channel is None:
|
||||||
|
self.logger.exception(f"Do not have channel to reply to!")
|
||||||
|
else:
|
||||||
|
# await self.reply_channel.send(embed=Embed().add_field(name="WikiLinks", value=result.reply))
|
||||||
|
self.logger.debug('TODO: should reply here!')
|
||||||
|
|
||||||
|
|
||||||
|
def good_bot(self, client:SocketModeClient, req: SocketModeRequest):
|
||||||
|
self.logger.info('Got told we are a good bot ^_^')
|
||||||
|
self.react('heart', client, req)
|
||||||
|
self.react('heavy_plus_sign', client, req)
|
||||||
|
self.react('fire', client, req)
|
||||||
|
self.react('heavy_equals_sign', client, req)
|
||||||
|
self.react('heart_on_fire', client, req)
|
||||||
|
|
||||||
|
def react(self, emoji:str, client:SocketModeClient, req: SocketModeRequest, remove:bool=False):
|
||||||
|
if remove:
|
||||||
|
client.web_client.reactions_remove(
|
||||||
|
name=emoji,
|
||||||
|
channel=req.payload["event"]["channel"],
|
||||||
|
timestamp=req.payload["event"]["ts"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
client.web_client.reactions_add(
|
||||||
|
name=emoji,
|
||||||
|
channel=req.payload["event"]["channel"],
|
||||||
|
timestamp=req.payload["event"]["ts"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self, creds: Optional[Slack_Creds] = None):
|
||||||
|
if creds is None:
|
||||||
|
creds = self.creds
|
||||||
|
else:
|
||||||
|
self.creds = creds
|
||||||
|
|
||||||
|
self.web_client, self.socket_client = self._init_client(creds)
|
||||||
|
|
||||||
|
self.socket_client.socket_mode_request_listeners.append(self.handle_event)
|
||||||
|
|
||||||
|
self.logger.debug("Connecting...")
|
||||||
|
|
||||||
|
self.socket_client.connect()
|
||||||
|
|
||||||
|
self.logger.debug(f"Got Reply Channel ID {self.reply_channel} for {self.reply_channel_name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.logger.info("Slack Client Listening")
|
||||||
|
Event().wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.logger.info("Quitting Slack client!")
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SlackMessage:
|
||||||
|
content: str # the actual content of the message
|
||||||
|
user_id: str
|
||||||
|
channel_id: str
|
||||||
|
channel: str # currently expected to be passed at instantiation because the client keeps a reverse index. bad information hiding i know.
|
||||||
|
timestamp: str
|
||||||
|
"""The unix epoch string timestamp stored as a slack event's ts attribute"""
|
||||||
|
|
||||||
|
# these need to be filled in after instantiation by passing a webclient to the completion methods
|
||||||
|
avatar: str = '' # URL of image
|
||||||
|
permalink: str = ''
|
||||||
|
author:str = '' # display name
|
||||||
|
|
||||||
|
_complete:bool=False
|
||||||
|
|
||||||
|
def get_permalink(self, client: WebClient):
|
||||||
|
permalink = client.chat_getPermalink(channel=self.channel_id, message_ts=self.timestamp)
|
||||||
|
self.permalink = permalink['permalink']
|
||||||
|
|
||||||
|
def get_user(self, client:WebClient):
|
||||||
|
user_info = client.users_info(user=self.user_id)
|
||||||
|
self.avatar = user_info['user']['profile']['image_192']
|
||||||
|
self.author = user_info['user']['profile']['display_name']
|
||||||
|
|
||||||
|
def complete(self, client:WebClient):
|
||||||
|
self.get_permalink(client)
|
||||||
|
self.get_user(client)
|
||||||
|
self._complete = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date_sent(self) -> datetime:
|
||||||
|
return datetime.fromtimestamp(float(self.timestamp))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def argparser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="slack_bot",
|
||||||
|
description="A slack bot for posting messages with wikilinks to an associated mediawiki wiki"
|
||||||
|
)
|
||||||
|
parser.add_argument('-d', '--directory', default='/etc/wikibot/', type=Path,
|
||||||
|
help="Directory that stores credential files and logs")
|
||||||
|
parser.add_argument('-w', '--wiki', help="URL of wiki", type=str)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
directory = Path(args.directory)
|
||||||
|
log_dir = directory / "logs"
|
||||||
|
|
||||||
|
slack_creds = Slack_Creds.from_json(directory / 'slack_creds.json')
|
||||||
|
wiki_creds = Mediawiki_Creds.from_json(directory / 'mediawiki_creds.json')
|
||||||
|
|
||||||
|
wiki = Wiki(url=args.wiki, log_dir=log_dir, creds=wiki_creds)
|
||||||
|
wiki.login(wiki_creds)
|
||||||
|
|
||||||
|
client = SlackClient(creds=slack_creds, wiki=wiki, log_dir=log_dir)
|
||||||
|
client.run()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -53,3 +53,16 @@ class Mediawiki_Creds:
|
||||||
with open(path, 'r') as jfile:
|
with open(path, 'r') as jfile:
|
||||||
creds = json.load(jfile)
|
creds = json.load(jfile)
|
||||||
return Mediawiki_Creds(**creds)
|
return Mediawiki_Creds(**creds)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Slack_Creds:
|
||||||
|
app_token:str
|
||||||
|
bot_token:str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, path:Path) -> 'Slack_Creds':
|
||||||
|
"""jesus christ this package is so sloppy"""
|
||||||
|
with open(path, 'r') as jfile:
|
||||||
|
creds = json.load(jfile)
|
||||||
|
return Slack_Creds(**creds)
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Class for interfacing with mediawiki
|
Class for interfacing with mediawiki
|
||||||
"""
|
"""
|
||||||
from typing import List, Optional
|
from typing import List, Optional, TYPE_CHECKING
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
@ -16,6 +16,9 @@ import requests
|
||||||
from discord.message import Message, Embed
|
from discord.message import Message, Embed
|
||||||
import pdb
|
import pdb
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from wiki_postbot.clients.slack import SlackMessage
|
||||||
|
|
||||||
# creds = Mediawiki_Creds.from_json('mediawiki_creds.json')
|
# creds = Mediawiki_Creds.from_json('mediawiki_creds.json')
|
||||||
|
|
||||||
|
|
||||||
|
@ -170,6 +173,39 @@ class Wiki:
|
||||||
else:
|
else:
|
||||||
return Result(ok=False, log=f"Got exceptions: {errored_pages}")
|
return Result(ok=False, log=f"Got exceptions: {errored_pages}")
|
||||||
|
|
||||||
|
def handle_slack(self, msg:'SlackMessage') -> Result:
|
||||||
|
"""
|
||||||
|
Brooooo it's getting laaaaaaaaaate and I think it's obvious how this should be refactored
|
||||||
|
but I want to play some zeldaaaaaaaaaaaa so I am copy and pasting for now
|
||||||
|
"""
|
||||||
|
self.login(self.creds)
|
||||||
|
# Get message in mediawiki template formatting
|
||||||
|
template_str = TemplateMessage.format_slack(msg)
|
||||||
|
|
||||||
|
# parse wikilinks, add to each page
|
||||||
|
wikilinks = Wikilink.parse(msg.content)
|
||||||
|
errored_pages = []
|
||||||
|
for link in wikilinks:
|
||||||
|
if link.section is None:
|
||||||
|
section = "Slack"
|
||||||
|
else:
|
||||||
|
section = link.section
|
||||||
|
|
||||||
|
res = self.insert_text(link.link, section, template_str)
|
||||||
|
if res.json()['edit']['result'] != 'Success':
|
||||||
|
errored_pages.append(res.json())
|
||||||
|
|
||||||
|
# Add to index page (only once)
|
||||||
|
self.add_to_index(template_str)
|
||||||
|
|
||||||
|
if len(errored_pages) == 0:
|
||||||
|
# gather links for a reply
|
||||||
|
reply = '\n'.join([f"[{l.link}]({urljoin(self.url, l.link.replace(' ', '_'))})" for l in wikilinks])
|
||||||
|
|
||||||
|
return Result(ok=True, log=f"Successfully posted message to {[l.link for l in wikilinks]}", reply=reply)
|
||||||
|
else:
|
||||||
|
return Result(ok=False, log=f"Got exceptions: {errored_pages}")
|
||||||
|
|
||||||
def add_to_index(self, message):
|
def add_to_index(self, message):
|
||||||
section = datetime.today().strftime("%y-%m-%d")
|
section = datetime.today().strftime("%y-%m-%d")
|
||||||
self.insert_text(page=self.index_page, section=section, text=message)
|
self.insert_text(page=self.index_page, section=section, text=message)
|
||||||
|
|
|
@ -64,3 +64,11 @@ def main():
|
||||||
|
|
||||||
service = Service(wiki_url=args.wiki, directory=args.directory)
|
service = Service(wiki_url=args.wiki, directory=args.directory)
|
||||||
service.install_service()
|
service.install_service()
|
||||||
|
|
||||||
|
def main_slack():
|
||||||
|
parser = argparser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
service = Service(wiki_url=args.wiki, directory=args.directory)
|
||||||
|
service.bot_script = shutil.which('slack_bot')
|
||||||
|
service.install_service()
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
"""
|
"""
|
||||||
Templates for representing different kinds of messages on mediawiki
|
Templates for representing different kinds of messages on mediawiki
|
||||||
"""
|
"""
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from wiki_postbot.formats.wiki import WikiPage
|
from wiki_postbot.formats.wiki import WikiPage
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from discord.message import Message
|
from discord.message import Message
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from wiki_postbot.clients.slack import SlackMessage
|
||||||
|
|
||||||
class WikiTemplate(WikiPage):
|
class WikiTemplate(WikiPage):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -37,6 +40,18 @@ class TemplateMessage(WikiTemplate):
|
||||||
"}}"
|
"}}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def format_slack(cls, msg:'SlackMessage') -> str:
|
||||||
|
return (
|
||||||
|
"{{Message\n"
|
||||||
|
f"|Author={msg.author}\n"
|
||||||
|
f"|Avatar={msg.avatar}\n"
|
||||||
|
f"|Date Sent={msg.date_sent.strftime('%y-%m-%d %H:%M:%S')}\n"
|
||||||
|
f"|Channel={msg.channel}\n"
|
||||||
|
f"|Text={msg.content}\n"
|
||||||
|
f"|Link={msg.permalink}\n"
|
||||||
|
"}}"
|
||||||
|
)
|
||||||
|
|
||||||
#
|
#
|
||||||
# template_message = TemplateMessage.from_source(
|
# template_message = TemplateMessage.from_source(
|
||||||
|
|
Loading…
Reference in a new issue