diff --git a/tests/test_patterns/test_wikilinks.py b/tests/test_patterns/test_wikilinks.py index ddbc52e..f4a0859 100644 --- a/tests/test_patterns/test_wikilinks.py +++ b/tests/test_patterns/test_wikilinks.py @@ -37,6 +37,10 @@ class TestStr: "[[^{3}Link]]", 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: @@ -121,6 +125,17 @@ def test_nback_range_end(test_string, expected): assert len(wl) == 1 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(): pass diff --git a/wiki_postbot/clients/discord_client.py b/wiki_postbot/clients/discord_client.py index a2bd000..ddaed8a 100644 --- a/wiki_postbot/clients/discord_client.py +++ b/wiki_postbot/clients/discord_client.py @@ -1,10 +1,12 @@ import discord -from discord import Client, Intents -from wiki_postbot.creds import Discord_Creds +from discord import Client, Intents, Embed, Message +from wiki_postbot.creds import Discord_Creds, Mediawiki_Creds from wiki_postbot.patterns.wikilink import Wikilink +from wiki_postbot.interfaces.mediawiki import Wiki from discord.ext import commands from discord import Emoji +import pdb # # intents = Intents.default() # 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: intents = Intents.default() 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 - super(MyClient, self).__init__(intents=intents, **kwargs) + super(DiscordClient, self).__init__(intents=intents, **kwargs) async def on_ready(self): print('Logged on as', self.user) - async def on_message(self, message): + + async def on_message(self, message:discord.message.Message): print(message) # don't respond to ourselves if message.author == self.user: return + + if 'good bot' in message.content: + await message.add_reaction("❤️🔥") + wl = Wikilink.parse(message.content) 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): print('debug command') @@ -77,9 +119,13 @@ class MyClient(Client): if __name__ == "__main__": - creds = Discord_Creds.from_json('discord_creds.json') - # client = MyClient() - # client.run(creds.token) + discord_creds = Discord_Creds.from_json('discord_creds.json') + wiki_creds = Mediawiki_Creds.from_json('mediawiki_creds.json') + 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) diff --git a/wiki_postbot/formats/wiki.py b/wiki_postbot/formats/wiki.py index c8361ec..139da82 100644 --- a/wiki_postbot/formats/wiki.py +++ b/wiki_postbot/formats/wiki.py @@ -2,4 +2,16 @@ Helper functions for dealing with wiki syntax as well as format 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) \ No newline at end of file diff --git a/wiki_postbot/interfaces/mediawiki.py b/wiki_postbot/interfaces/mediawiki.py index 836a670..ca2204d 100644 --- a/wiki_postbot/interfaces/mediawiki.py +++ b/wiki_postbot/interfaces/mediawiki.py @@ -5,21 +5,27 @@ from typing import List from urllib.parse import urljoin from dataclasses import dataclass 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 +from discord.message import Message, Embed +import pdb # creds = Mediawiki_Creds.from_json('mediawiki_creds.json') - - 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.api_url = urljoin(self.url, api_suffix) 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 sess = requests.Session() @@ -48,9 +54,8 @@ class Wiki: ) assert login_result.json()['login']['result'] == "Success" self.sess = sess - return sess - def get_page_content(self, page:str) -> str: + def get_page(self, page:str) -> WikiPage: content = self.sess.get( self.api_url, @@ -62,10 +67,21 @@ class Wiki: 'format':'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): + # 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( self.api_url, params={ @@ -76,15 +92,67 @@ class Wiki: verify=False ).json()['query']['tokens']['csrftoken'] - result = self.sess.post( - self.api_url, - data={ - "action":"edit", - "title":page, - "section":"new", - "sectiontitle":section, - "appendtext":text, - "format":"json", - "token":token - } - ) + if matching_section >= 0: + print(f'found matching section {matching_section}') + result = self.sess.post( + self.api_url, + data={ + "action":"edit", + "title":page, + "section":str(matching_section), + "appendtext":text, + "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) diff --git a/wiki_postbot/patterns/wikilink.py b/wiki_postbot/patterns/wikilink.py index 7770b41..7ad4ff2 100644 --- a/wiki_postbot/patterns/wikilink.py +++ b/wiki_postbot/patterns/wikilink.py @@ -139,6 +139,9 @@ class Wikilink(Pattern): elif isinstance(nback, pp.ParseResults): nback = NBack(**dict(nback)) + if isinstance(section, pp.ParseResults): + section = section[0] + self.nback = nback self.predicate = predicate self.object = object @@ -159,8 +162,12 @@ class Wikilink(Pattern): # main wikilink subject text link = pp.Word(pp.printables+ " ", excludeChars="#[]{}|") + # optional page section + hash = pp.Literal("#").suppress() + section = hash + link + # Combine all - parser = lbracket + pp.Optional(nback) + link("link") + rbracket + parser = lbracket + pp.Optional(nback) + link("link") + pp.Optional(section("section")) + rbracket return parser @classmethod diff --git a/wiki_postbot/templates/wiki.py b/wiki_postbot/templates/wiki.py new file mode 100644 index 0000000..d8b4fc2 --- /dev/null +++ b/wiki_postbot/templates/wiki.py @@ -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=""" + +{{Message +|Author= +|Avatar= +|Date Sent= +|Channel=(Optional) +|Text= +|Link= +}} + + + + +{{#subobject:{{{Author}}}-{{{Date Sent}}} + |Message topic={{PAGENAME}} + |Has author={{{Author}}} + |Date sent={{{Date Sent}}} + |Has URL={{{Link}}} + |Contains text={{{Text}}} +}} + +{{{Author}}}#{{{Channel|}}}[{{{Link}}} {{{Date Sent}}}] + +{{{Text}}} + + + +""" +) \ No newline at end of file
+{{Message +|Author= +|Avatar= +|Date Sent= +|Channel=(Optional) +|Text= +|Link= +}} +