From 1f575f03114a2255c2b5676e671e9eb57eb86a4c Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 25 Feb 2021 14:06:33 -0600 Subject: [PATCH] Added reading article support from Socializer wall posts --- changelog.md | 1 + src/interactors/postDisplayer.py | 15 ++++++ src/presenters/displayPosts/__init__.py | 1 + src/presenters/displayPosts/article.py | 47 ++++++++++++++++ src/presenters/displayPosts/basePost.py | 7 ++- src/sessionmanager/session.py | 1 - src/views/dialogs/postDisplay.py | 71 ++++++++++++++++++++++++- 7 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 src/presenters/displayPosts/article.py diff --git a/changelog.md b/changelog.md index 715dae8..d7ffff9 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ ### new additions +* It is now possible to read an article from a wall post. The article will be opened in a new dialog. This might work better in countries where VK is blocked as users no longer need to open the web browser. Unfortunately, as articles are mainly undocumented in the API, it is not possible to perform other actions besides reading them from Socializer. * the spelling correction module is able to add words to the dictionary so it will learn which words should start to ignore. ### bugfixes diff --git a/src/interactors/postDisplayer.py b/src/interactors/postDisplayer.py index c2aed83..4dfb2f5 100644 --- a/src/interactors/postDisplayer.py +++ b/src/interactors/postDisplayer.py @@ -218,6 +218,21 @@ class displayAudioInteractor(base.baseInteractor): post = self.view.get_audio() self.presenter.remove_from_library(post) +class displayArticleInteractor(base.baseInteractor): + + def set(self, control, value): + if not hasattr(self.view, control): + raise AttributeError("The control is not present in the view.") + getattr(self.view, control).SetValue(value) + + def install(self, *args, **kwargs): + super(displayArticleInteractor, self).install(*args, **kwargs) + pub.subscribe(self.set, self.modulename+"_set") + + def uninstall(self): + super(displayArticleInteractor, self).uninstall() + pub.unsubscribe(self.set, self.modulename+"_set") + class displayPollInteractor(base.baseInteractor): def set(self, control, value): diff --git a/src/presenters/displayPosts/__init__.py b/src/presenters/displayPosts/__init__.py index dcdbe5f..4fdbac1 100644 --- a/src/presenters/displayPosts/__init__.py +++ b/src/presenters/displayPosts/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from .basePost import * from .audio import * +from .article import * from .comment import * from .peopleList import * from .poll import * diff --git a/src/presenters/displayPosts/article.py b/src/presenters/displayPosts/article.py new file mode 100644 index 0000000..20b3053 --- /dev/null +++ b/src/presenters/displayPosts/article.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" Presenter to render an article from the VK mobile website. +this is an helper class to display an article within socializer, as opposed to opening a web browser and asking the user to get there. +""" +import logging +import re +from bs4 import BeautifulSoup +from presenters import base + +log = logging.getLogger(__file__) + +class displayArticlePresenter(base.basePresenter): + def __init__(self, session, postObject, view, interactor): + super(displayArticlePresenter, self).__init__(view=view, interactor=interactor, modulename="display_article") + self.session = session + self.post = postObject + self.load_article() + self.run() + + def load_article(self): + """ Loads the article in the interactor. + This function retrieves, by using the params defined in the VK API, the web version of the article and extracts some info from it. + this is needed because there are no public API for articles so far. + """ + article = self.post[0] + # By using the vk_api's session, proxy settings are applied, thus might work in blocked countries. + article_body = self.session.vk.session_object.http.get(article["view_url"]) + # Parse and extract text from all paragraphs. + # ToDo: Extract all links and set those as attachments. + soup = BeautifulSoup(article_body.text, "lxml") + # ToDo: Article extraction require testing to see if you can add more tags beside paragraphs. + msg = [p.get_text() for p in soup.find_all("p")] + msg = "\n\n".join(msg) + self.send_message("set", control="article_view", value=msg) + self.send_message("set_title", value=article["title"]) + # Retrieve views count + views = soup.find("div", class_="articleView__views_info") + # This might return None if VK changes anything, so let's avoid errors. + if views == None: + views = str(-1) + else: + views = views.text + # Find the integer and remove the words from the string. + numbers = re.findall(r'\d+', views) + if len(numbers) != 0: + views = numbers[0] + self.send_message("set", control="views", value=views) \ No newline at end of file diff --git a/src/presenters/displayPosts/basePost.py b/src/presenters/displayPosts/basePost.py index d2a84e3..6b81f9d 100644 --- a/src/presenters/displayPosts/basePost.py +++ b/src/presenters/displayPosts/basePost.py @@ -15,7 +15,7 @@ from extra import SpellChecker, translator from mysc.thread_utils import call_threaded from presenters import base from presenters.createPosts.basePost import createPostPresenter -from . import audio, poll +from . import audio, poll, article log = logging.getLogger(__file__) @@ -351,10 +351,9 @@ class displayPostPresenter(base.basePresenter): elif attachment["type"] == "poll": a = poll.displayPollPresenter(session=self.session, poll=attachment, interactor=interactors.displayPollInteractor(), view=views.displayPoll()) elif attachment["type"] == "article": - output.speak(_("Opening Article in web browser..."), True) - webbrowser.open_new_tab(attachment["article"]["url"]) + a = article.displayArticlePresenter(session=self.session, postObject=[attachment["article"]], interactor=interactors.displayArticleInteractor(), view=views.displayArticle()) else: - log.debug("Unhandled attachment: %r" % (attachment,)) + log.error("Unhandled attachment: %r" % (attachment,)) def __del__(self): if hasattr(self, "worker"): diff --git a/src/sessionmanager/session.py b/src/sessionmanager/session.py index 5d68263..43a0430 100644 --- a/src/sessionmanager/session.py +++ b/src/sessionmanager/session.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """ Session object for Socializer. A session is the only object to call VK API methods, save settings and access to the cache database and sound playback mechanisms. """ -from __future__ import unicode_literals import os import logging import warnings diff --git a/src/views/dialogs/postDisplay.py b/src/views/dialogs/postDisplay.py index f1d45d4..0ab189e 100644 --- a/src/views/dialogs/postDisplay.py +++ b/src/views/dialogs/postDisplay.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals import wx import widgetUtils @@ -283,6 +282,76 @@ class displayAudio(widgetUtils.BaseDialog): def get_audio(self): return self.list.GetSelection() +class displayArticle(widgetUtils.BaseDialog): + def __init__(self, *args, **kwargs): + super(displayArticle, self).__init__(parent=None, *args, **kwargs) + self.panel = wx.Panel(self, -1) + self.sizer = wx.BoxSizer(wx.VERTICAL) + article_view_box = self.create_article_view() + self.sizer.Add(article_view_box, 0, wx.ALL, 5) + views_box = self.create_views_control() + self.sizer.Add(views_box, 0, wx.ALL, 5) + attachments_box = self.create_attachments() + self.sizer.Add(attachments_box, 0, wx.ALL, 5) + self.attachments.list.Enable(False) + self.create_tools_button() + self.sizer.Add(self.tools, 0, wx.ALL, 5) + self.sizer.Add(self.create_dialog_buttons()) + self.done() + + def done(self): + self.panel.SetSizer(self.sizer) + self.SetClientSize(self.sizer.CalcMin()) + + def create_article_view(self, label=_("Article")): + lbl = wx.StaticText(self.panel, -1, label) + self.article_view = wx.TextCtrl(self.panel, -1, size=(730, -1), style=wx.TE_READONLY|wx.TE_MULTILINE|wx.BORDER_SIMPLE) + selectId = wx.NewId() + self.Bind(wx.EVT_MENU, self.onSelect, id=selectId) + self.accel_tbl = wx.AcceleratorTable([ + (wx.ACCEL_CTRL, ord('A'), selectId),]) + self.SetAcceleratorTable(self.accel_tbl) + box = wx.BoxSizer(wx.HORIZONTAL) + box.Add(lbl, 0, wx.ALL, 5) + box.Add(self.article_view, 0, wx.ALL, 5) + return box + + def onSelect(self, event): + self.article_view.SelectAll() + + def create_views_control(self): + lbl = wx.StaticText(self.panel, -1, _("Views")) + self.views = wx.TextCtrl(self.panel, -1, style=wx.TE_READONLY|wx.TE_MULTILINE) + box = wx.BoxSizer(wx.HORIZONTAL) + box.Add(lbl, 0, wx.ALL, 5) + box.Add(self.views, 0, wx.ALL, 5) + return box + + def create_tools_button(self): + self.tools = wx.Button(self.panel, -1, _("Actions")) + + def create_dialog_buttons(self): + self.close = wx.Button(self.panel, wx.ID_CANCEL, _("Close")) + return self.close + + def create_attachments(self): + lbl = wx.StaticText(self.panel, -1, _("Attachments")) + self.attachments = widgetUtils.list(self.panel, _("Type"), _("Title"), style=wx.LC_REPORT) + box = wx.BoxSizer(wx.HORIZONTAL) + box.Add(lbl, 0, wx.ALL, 5) + box.Add(self.attachments.list, 0, wx.ALL, 5) + return box + + def set_article(self, text): + if hasattr(self, "article_view"): + self.article_view.ChangeValue(text) + else: + return False + + def insert_attachments(self, attachments): + for i in attachments: + self.attachments.insert_item(False, *i) + class displayFriendship(widgetUtils.BaseDialog): def __init__(self): super(displayFriendship, self).__init__(parent=None)