From fa87e033939a90276de65708cdcbead061dcacde Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Oct 10 2017 10:00:19 +0000 Subject: [PATCH 1/2] Add structure for the IRC widget --- diff --git a/hubs/default_config.py b/hubs/default_config.py index fe46f94..b0e805d 100644 --- a/hubs/default_config.py +++ b/hubs/default_config.py @@ -63,6 +63,7 @@ WIDGETS = [ 'hubs.widgets.github_pr:GitHubPRs', 'hubs.widgets.githubissues:GitHubIssues', 'hubs.widgets.halp:Halp', + 'hubs.widgets.irc:IRC', 'hubs.widgets.meetings:Meetings', 'hubs.widgets.memberships:Memberships', 'hubs.widgets.pagure_pr:PagurePRs', diff --git a/hubs/static/client/app/widgets/irc/Config.js b/hubs/static/client/app/widgets/irc/Config.js new file mode 100644 index 0000000..9b408b6 --- /dev/null +++ b/hubs/static/client/app/widgets/irc/Config.js @@ -0,0 +1,25 @@ +import React from 'react'; + + +export default class Config extends React.Component { + + constructor(props) { + super(props); + this.state = { + }; + } + + render() { + return ( +
+

The content of the IRC widget config panel goes here.

+

Available URLs:

+ +
+ ); + } + +} diff --git a/hubs/static/client/app/widgets/irc/Widget.js b/hubs/static/client/app/widgets/irc/Widget.js new file mode 100644 index 0000000..96dc443 --- /dev/null +++ b/hubs/static/client/app/widgets/irc/Widget.js @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import WidgetChrome from '../../components/WidgetChrome'; + +export default class Widget extends React.Component { + + constructor(props) { + super(props); + this.state = { + isLoading: false + }; + } + + render() { + return ( + +
+ The content of the IRC widget goes here. +
+
+ ); + } +} +Widget.propTypes = { + widget: PropTypes.object.isRequired, + editMode: PropTypes.bool, + needsUpdate: PropTypes.bool, +} diff --git a/hubs/widgets/irc/__init__.py b/hubs/widgets/irc/__init__.py new file mode 100644 index 0000000..1866f21 --- /dev/null +++ b/hubs/widgets/irc/__init__.py @@ -0,0 +1,42 @@ +from __future__ import unicode_literals, absolute_import + +import logging + +import flask + +from hubs.widgets.base import Widget +from .views import check_avail_view, register_nick_view + + +log = logging.getLogger('hubs.widgets') + + +class IRC(Widget): + + name = "irc" + label = "Chat" + position = "both" + parameters = [] + is_react = True + views_module = ".views" + + def register_routes(self, app): + super(IRC, self).register_routes(app) + # Add the views that are not specific to this instance. + # They won't be automatically picked up since they don't + # inherit from WidgetView. + rule = "/w/%s/check-avail" % self.name + endpoint = "%s_check_avail" % self.name + app.add_url_rule(rule, endpoint=endpoint, view_func=check_avail_view) + rule = "/w/%s/register-nick" % self.name + endpoint = "%s_register_nick" % self.name + app.add_url_rule(rule, endpoint=endpoint, view_func=register_nick_view) + + def get_props(self, instance, *args, **kwargs): + props = super(IRC, self).get_props(instance, *args, **kwargs) + # Add the URLs to endpoints that the Javascript UI will use. + props["urls"] = dict( + checkAvail=flask.url_for("irc_check_avail"), + registerNickEmail=flask.url_for("irc_register_nick"), + ) + return props diff --git a/hubs/widgets/irc/utils.py b/hubs/widgets/irc/utils.py new file mode 100644 index 0000000..c6b14bc --- /dev/null +++ b/hubs/widgets/irc/utils.py @@ -0,0 +1,40 @@ +from __future__ import unicode_literals, absolute_import + + +import flask + + +class SessionEventLogger(object): + """ + Logs events in the Flask session. + """ + # This is just a suggestion for a way to manage the server-side + # state using the Flask session. + # I'm sure there are better ways to chain actions than using the + # `cb_next` argument, but Sayan knows best what the requirements + # are. + + namespace = "widget-irc-events" + + def on(self, event_name, cb_next=None): + def callback(connection, event): + if self.namespace not in flask.session: + flask.session[self.namespace] = set() + flask.session[self.namespace].add(event_name) + flask.session.modified = True + if cb_next: + return cb_next(connection) + else: + return "NO_ERROR" + return callback + + def has_happened(self, event_name): + if self.namespace not in flask.session: + return False + return event_name in flask.session[self.namespace] + + def forget(self, event_name): + if self.namespace not in flask.session: + return + flask.session[self.namespace].remove(event_name) + flask.session.modified = True diff --git a/hubs/widgets/irc/views.py b/hubs/widgets/irc/views.py new file mode 100644 index 0000000..2b1edb7 --- /dev/null +++ b/hubs/widgets/irc/views.py @@ -0,0 +1,103 @@ +from __future__ import unicode_literals, absolute_import + +import irc.client +import flask +from .utils import SessionEventLogger + + +def check_avail_view(): + nickname = flask.request.args.get("nickname") + if not nickname: + return flask.jsonify({ + "status": "ERROR", + "msg": "You must supply a nickname to check." + }) + reactor = irc.client.Reactor() + try: + server = reactor.server().connect( + 'irc.freenode.net', + 6667, + nickname) + except irc.client.ServerConnectionError: + print(sys.exc_info()[1]) + return flask.jsonify({ + 'status': 'ERROR' + }) + events = SessionEventLogger() + server.add_global_handler('nicknameinuse', events.on("nicknameinuse")) + server.add_global_handler('welcome', events.on("nickav_connect")) + while server.connected: + if events.has_happened("nicknameinuse"): + events.forget("nicknameinuse") + server.close() + return flask.jsonify({ + 'status': 'ERROR', + 'msg': 'Nickname in use', + }) + if events.has_happened("nickav_connect"): + events.forget("nickav_connect") + server.close() + return flask.jsonify({ + 'status': 'OK', + 'msg': 'Nickname in available', + }) + reactor.process_once() + + +def register_nick_view(): + request_data = flask.request.get_json() + try: + nickname = request_data["nickname"] + password = request_data["password"] + email = request_data["email"] + except KeyError as e: + return flask.jsonify({ + "status": "ERROR", + "msg": str(e), + }) + reactor = irc.client.Reactor() + try: + server = reactor.server().connect( + 'irc.freenode.net', + 6667, + nickname + ) + except irc.client.ServerConnectionError: + return flask.jsonify({ + 'status': 'ERROR' + }) + + def register_nick(connection): + connection.privmsg( + "NickServ", + "register {password} {email}".format( + password=password, email=email) + ) + return 'NO_ERROR' + + server.add_global_handler('nicknameinuse', events.on("nicknameinuse")) + server.add_global_handler( + 'welcome', events.on("welcome", cb_next=register_nick)) + # TODO: check the answer from NickServ + + while server.connected: + if events.has_happened("nicknameinuse"): + events.forget("nicknameinuse") + server.close() + return flask.jsonify({ + 'status': 'ERROR', + 'msg': 'Nickname in use' + }) + + if events.has_happened("welcome"): + events.forget("welcome") + # TODO: this should actually be a check for a positive answer + # from NickServ + server.close() + return flask.jsonify({ + 'status': 'OK', + 'msg': 'Nickname has been registered', + }) + + reactor.process_once() +register_nick_view.methods = ["POST"] From fe6367b708656eae13bef3e18a68fe65e7d77a9a Mon Sep 17 00:00:00 2001 From: Sayan Chowdhury Date: Oct 10 2017 15:46:52 +0000 Subject: [PATCH 2/2] widgets.irc: Migrate the choose nickname to React Signed-off-by: Sayan Chowdhury --- diff --git a/hubs/static/client/app/widgets/irc/ModalChooseNickname.js b/hubs/static/client/app/widgets/irc/ModalChooseNickname.js new file mode 100644 index 0000000..c8bace7 --- /dev/null +++ b/hubs/static/client/app/widgets/irc/ModalChooseNickname.js @@ -0,0 +1,75 @@ +import React from 'react'; +import Spinner from '../../components/Spinner'; +import Modal from '../../components/Modal'; + +export default class ModalChooseNickname extends React.Component { + + constructor(props) { + super(props); + this.state = { + isNickValidCheck: false, + isNickValid: null, + validNickName: null, + formData: { + nick: null + } + }; + } + + handleChooseNickChange(nick) { + const formData = this.state.formData; + this.setState({isNickValidCheck: true}); + this.serverRequest = $.ajax({ + url: this.props.urls.checkAvail, + method: 'POST', + data: formData, + dataType: 'json', + success: function (data) { + this.setState({ + isNickValidCheck: false, + isNickValid: true, + validNickName: data.nickname + }); + }.bind(this), + error: function(xhr, status, err) { + this.setState({ + isNickValidCheck: false, + isNickValid: false + }); + }.bind(this) + }); + } + + render () { + let footer; + footer = null; + + return ( + +
+ Choose a nickname +
+
+
+ +
+
+ +
+
+
+
+
+ ); + } +} diff --git a/hubs/static/client/app/widgets/irc/Widget.js b/hubs/static/client/app/widgets/irc/Widget.js index 96dc443..c9cca91 100644 --- a/hubs/static/client/app/widgets/irc/Widget.js +++ b/hubs/static/client/app/widgets/irc/Widget.js @@ -1,14 +1,51 @@ import React from 'react'; import PropTypes from 'prop-types'; import WidgetChrome from '../../components/WidgetChrome'; +import { + defineMessages, + FormattedMessage, + } from 'react-intl'; + +import ModalChooseNickname from './ModalChooseNickname'; + +const messages = defineMessages({ + choose_nickname: { + id: "hubs.widgets.irc.choose_nickname", + defaultMessage: "Choose your IRC nickname." + }, +}); export default class Widget extends React.Component { + propTypes: { + widget: PropTypes.object.isRequired, + editMode: PropTypes.bool, + needsUpdate: PropTypes.bool, + } + constructor(props) { super(props); this.state = { - isLoading: false + isLoading: false, + nickRegisterModalOpen: false }; + this.openNickRegisterModal = this.openNickRegisterModal.bind(this); + this.openNickRegisterModalClose = this.openNickRegisterModalClose.bind(this); + this.getNickRegisterModalTitle = this.getNickRegisterModalTitle.bind(this); + } + + openNickRegisterModal(e) { + this.setState({nickRegisterModalOpen: true}); + } + + onNickRegisterModalClose(e) { + this.setState({nickRegisterModalOpen: false}); + } + + getNickRegisterModalTitle() { + return ( + + ); } render() { @@ -19,13 +56,16 @@ export default class Widget extends React.Component { >
The content of the IRC widget goes here. +
+ { this.state.nickRegisterModalOpen && + + } ); } } -Widget.propTypes = { - widget: PropTypes.object.isRequired, - editMode: PropTypes.bool, - needsUpdate: PropTypes.bool, -}