working version of wikibot!!!!

This commit is contained in:
sneakers-the-rat 2022-10-16 18:35:49 -07:00
parent 079669a6d3
commit 5e60e5f8b0
6 changed files with 245 additions and 31 deletions

View file

@ -37,6 +37,10 @@ class TestStr:
"[[^{3}Link]]", "[[^{3}Link]]",
Wikilink("Link", nback=NBack(end=3)) Wikilink("Link", nback=NBack(end=3))
) )
section = (
"[[^{1,3}Link#With section]]",
Wikilink("Link", nback=NBack(1,3), section="With section")
)
def pad_garbage(string:str) -> str: def pad_garbage(string:str) -> str:
@ -121,6 +125,17 @@ def test_nback_range_end(test_string, expected):
assert len(wl) == 1 assert len(wl) == 1
assert wl[0] == expected assert wl[0] == expected
@pytest.mark.parametrize(
"test_string,expected",
[TestStr.section])
def test_section(test_string, expected):
test_string = pad_garbage(test_string)
wl = Wikilink.parse(test_string)
# pdb.set_trace()
assert len(wl) == 1
assert wl[0] == expected
def test_triplet_full(): def test_triplet_full():
pass pass

View file

@ -1,10 +1,12 @@
import discord import discord
from discord import Client, Intents from discord import Client, Intents, Embed, Message
from wiki_postbot.creds import Discord_Creds 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 discord.ext import commands from discord.ext import commands
from discord import Emoji from discord import Emoji
import pdb
# #
# intents = Intents.default() # intents = Intents.default()
# intents.message_content = True # intents.message_content = True
@ -38,30 +40,70 @@ from discord import Emoji
class MyClient(Client): class DiscordClient(Client):
def __init__(self, intents=None, debug:bool=False, **kwargs): def __init__(self, wiki:Wiki, intents=None, debug:bool=False, **kwargs):
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
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
super(MyClient, self).__init__(intents=intents, **kwargs) super(DiscordClient, self).__init__(intents=intents, **kwargs)
async def on_ready(self): async def on_ready(self):
print('Logged on as', self.user) print('Logged on as', self.user)
async def on_message(self, message):
async def on_message(self, message:discord.message.Message):
print(message) print(message)
# don't respond to ourselves # don't respond to ourselves
if message.author == self.user: if message.author == self.user:
return return
if 'good bot' in message.content:
await message.add_reaction("❤️‍🔥")
wl = Wikilink.parse(message.content) wl = Wikilink.parse(message.content)
if len(wl)>0: if len(wl)>0:
await message.channel.send(f"Wikilinks detected: \n" + '\n'.join([str(l) for l in wl])) if self.debug:
await message.channel.send(f"Wikilinks detected: \n" + '\n'.join([str(l) for l in wl]))
await message.add_reaction("")
try:
result = self.wiki.handle_discord(message)
ok = result.ok
except:
# TODO: Log here!
result = None
ok = False
if ok:
await message.remove_reaction("", self.user)
await message.add_reaction("")
else:
await message.remove_reaction("", self.user)
await message.add_reaction("")
if result and result.reply:
await message.channel.send(embed=Embed().add_field(name="WikiLinks", value=result.reply))
# TODO: Logging!
# def add_links(self, links:Wikilink, msg:discord.message.Message):
# 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.edit(content=message.content, embed=Embed().add_field(name="Links", value="There are [links](https://example.com) in here"))
# #await message.channel.send("Bot is testing if it can [make links](https://example.com)")
async def debug(ctx: discord.ext.commands.Context, arg): async def debug(ctx: discord.ext.commands.Context, arg):
print('debug command') print('debug command')
@ -77,9 +119,13 @@ class MyClient(Client):
if __name__ == "__main__": if __name__ == "__main__":
creds = Discord_Creds.from_json('discord_creds.json') discord_creds = Discord_Creds.from_json('discord_creds.json')
# client = MyClient() wiki_creds = Mediawiki_Creds.from_json('mediawiki_creds.json')
# client.run(creds.token) wiki = Wiki(url="https://cscw.sciop.net")
wiki.login(wiki_creds)
client = DiscordClient(wiki=wiki)
client.run(discord_creds.token)
bot.run(creds.token) # bot.run(creds.token)

View file

@ -2,4 +2,16 @@
Helper functions for dealing with wiki syntax as well as format Helper functions for dealing with wiki syntax as well as format
output posts triggered by :class:`.action.WikiLink` output posts triggered by :class:`.action.WikiLink`
""" """
from dataclasses import dataclass
import wikitextparser as wtp
@dataclass
class WikiPage:
title:str
source:str
content:wtp.WikiText
@classmethod
def from_source(self, title, source) -> 'WikiPage':
content = wtp.parse(source)
return WikiPage(title, source, content)

View file

@ -5,21 +5,27 @@ from typing import List
from urllib.parse import urljoin from urllib.parse import urljoin
from dataclasses import dataclass from dataclasses import dataclass
from wiki_postbot.creds import Mediawiki_Creds from wiki_postbot.creds import Mediawiki_Creds
from wiki_postbot.formats.wiki import WikiPage
from wiki_postbot.templates.wiki import TemplateMessage
from wiki_postbot.patterns.wikilink import Wikilink
from wiki_postbot.actions import Result
from datetime import datetime
import requests import requests
from discord.message import Message, Embed
import pdb
# creds = Mediawiki_Creds.from_json('mediawiki_creds.json') # creds = Mediawiki_Creds.from_json('mediawiki_creds.json')
class Wiki: class Wiki:
def __init__(self, url:str, api_suffix:str="/api.php"): def __init__(self, url:str, api_suffix:str="/api.php", index_page="Discord Messages"):
self.url = url self.url = url
self.api_url = urljoin(self.url, api_suffix) self.api_url = urljoin(self.url, api_suffix)
self.sess = None self.sess = None
self.index_page = index_page
def login(self, creds:Mediawiki_Creds) -> requests.Session: def login(self, creds:Mediawiki_Creds):
# get token to log in # get token to log in
sess = requests.Session() sess = requests.Session()
@ -48,9 +54,8 @@ class Wiki:
) )
assert login_result.json()['login']['result'] == "Success" assert login_result.json()['login']['result'] == "Success"
self.sess = sess self.sess = sess
return sess
def get_page_content(self, page:str) -> str: def get_page(self, page:str) -> WikiPage:
content = self.sess.get( content = self.sess.get(
self.api_url, self.api_url,
@ -62,10 +67,21 @@ class Wiki:
'format':'json' 'format':'json'
} }
).json() ).json()
return content['parse']['wikitext'] return WikiPage.from_source(title=content['parse']['title'], source=content['parse']['wikitext'])
def insert_text(self, page, section, text): def insert_text(self, page, section, text):
# TODO: Move finding section IDs into the page class!
page_text = self.get_page(page)
sections = page_text.content.get_sections()
matching_section = -1
for i, page_section in enumerate(sections):
if page_section.title is not None and page_section.title.strip().lower() == section.lower():
matching_section = i
break
token = self.sess.get( token = self.sess.get(
self.api_url, self.api_url,
params={ params={
@ -76,15 +92,67 @@ class Wiki:
verify=False verify=False
).json()['query']['tokens']['csrftoken'] ).json()['query']['tokens']['csrftoken']
result = self.sess.post( if matching_section >= 0:
self.api_url, print(f'found matching section {matching_section}')
data={ result = self.sess.post(
"action":"edit", self.api_url,
"title":page, data={
"section":"new", "action":"edit",
"sectiontitle":section, "title":page,
"appendtext":text, "section":str(matching_section),
"format":"json", "appendtext":text,
"token":token "format":"json",
} "token":token
) }
)
else:
print('making new section')
result = self.sess.post(
self.api_url,
data={
"action":"edit",
"title":page,
"section":"new",
"sectiontitle":section,
"appendtext":text,
"format":"json",
"token":token
}
)
return result
def handle_discord(self, msg:Message) -> Result:
"""
Not being precious about this, just implementing
and will worry about generality later!
"""
# Get message in mediawiki template formatting
template_str = TemplateMessage.format_discord(msg)
# parse wikilinks, add to each page
wikilinks = Wikilink.parse(msg.content)
errored_pages = []
for link in wikilinks:
if link.section is None:
section = "Discord"
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):
section = datetime.today().strftime("%y-%m-%d")
self.insert_text(page=self.index_page, section=section, text=message)

View file

@ -139,6 +139,9 @@ class Wikilink(Pattern):
elif isinstance(nback, pp.ParseResults): elif isinstance(nback, pp.ParseResults):
nback = NBack(**dict(nback)) nback = NBack(**dict(nback))
if isinstance(section, pp.ParseResults):
section = section[0]
self.nback = nback self.nback = nback
self.predicate = predicate self.predicate = predicate
self.object = object self.object = object
@ -159,8 +162,12 @@ class Wikilink(Pattern):
# main wikilink subject text # main wikilink subject text
link = pp.Word(pp.printables+ " ", excludeChars="#[]{}|") link = pp.Word(pp.printables+ " ", excludeChars="#[]{}|")
# optional page section
hash = pp.Literal("#").suppress()
section = hash + link
# Combine all # Combine all
parser = lbracket + pp.Optional(nback) + link("link") + rbracket parser = lbracket + pp.Optional(nback) + link("link") + pp.Optional(section("section")) + rbracket
return parser return parser
@classmethod @classmethod

View file

@ -0,0 +1,66 @@
"""
Templates for representing different kinds of messages on mediawiki
"""
from wiki_postbot.formats.wiki import WikiPage
from abc import abstractmethod
from discord.message import Message
class WikiTemplate(WikiPage):
@abstractmethod
def format_discord(self, msg:Message) -> str:
"""
Format a discord message into a template string
"""
class TemplateMessage(WikiTemplate):
@classmethod
def format_discord(self, msg:Message) -> str:
return (
"{{Message\n"
f"|Author={msg.author.name}\n"
f"|Avatar={msg.author.avatar.url}\n"
f"|Date Sent={msg.created_at.strftime('%y-%m-%d %H:%M:%S')}\n"
f"|Channel={msg.channel}\n"
f"|Text={msg.content}\n"
f"|Link={msg.jump_url}\n"
"}}"
)
template_message = TemplateMessage.from_source(
title="Template:Message",
source="""<noinclude>
<pre>
{{Message
|Author=
|Avatar=
|Date Sent=
|Channel=(Optional)
|Text=
|Link=
}}
</pre>
</noinclude>
<includeonly>
{{#subobject:{{{Author}}}-{{{Date Sent}}}
|Message topic={{PAGENAME}}
|Has author={{{Author}}}
|Date sent={{{Date Sent}}}
|Has URL={{{Link}}}
|Contains text={{{Text}}}
}}
<div style="border: 1px solid black; border-radius: 5px;padding:5px"
>
<div style="display:flex; flex-direction:row; align-items:center; border-bottom:1px solid black; gap:10px; padding-bottom:5px;"><img src={{{Avatar|}}} style="width:30px;border-radius:10px"/><span style="font-weight:bold;">{{{Author}}}</span><span><nowiki>#</nowiki>{{{Channel|}}}</span><span style="font-style:italic;color:#999999">[{{{Link}}} {{{Date Sent}}}]</span></div>
<div>
{{{Text}}}
</div>
</div>
</includeonly>
"""
)