@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Make Notifications Realtime

Summary:
Adds the node.js Aphlict server, the flash Aphlict client, and some
supporting javascript. Built on top of - and requires - D2703 (which is still
in progress). Will likely work with no modification on top of the final
version, though.

The node server is currently run with

sudo node support/aphlict/server/aphlict_server.js

Test Plan: tested locally

Reviewers: epriestley

Reviewed By: epriestley

CC: allenjohnashton, keebuhm, aran, Korvin

Differential Revision: https://secure.phabricator.com/D2704

authored by

David Fisher and committed by
epriestley
f8f195b3 2bade93b

+233 -136
+2 -1
src/__celerity_resource_map__.php
··· 772 772 ), 773 773 'javelin-behavior-aphlict-listen' => 774 774 array( 775 - 'uri' => '/res/6388e057/rsrc/js/application/aphlict/behavior-aphlict-listen.js', 775 + 'uri' => '/res/7f4bc63b/rsrc/js/application/aphlict/behavior-aphlict-listen.js', 776 776 'type' => 'js', 777 777 'requires' => 778 778 array( ··· 780 780 1 => 'javelin-aphlict', 781 781 2 => 'javelin-util', 782 782 3 => 'javelin-stratcom', 783 + 4 => 'javelin-behavior-aphlict-dropdown', 783 784 ), 784 785 'disk' => '/rsrc/js/application/aphlict/behavior-aphlict-listen.js', 785 786 ),
+2
src/__phutil_library_map__.php
··· 736 736 'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php', 737 737 'PhabricatorNotificationBuilder' => 'applications/notification/builder/PhabricatorNotificationBuilder.php', 738 738 'PhabricatorNotificationController' => 'applications/notification/controller/PhabricatorNotificationController.php', 739 + 'PhabricatorNotificationIndividualController' => 'applications/notification/controller/PhabricatorNotificationIndividualController.php', 739 740 'PhabricatorNotificationPanelController' => 'applications/notification/controller/PhabricatorNotificationPanelController.php', 740 741 'PhabricatorNotificationQuery' => 'applications/notification/PhabricatorNotificationQuery.php', 741 742 'PhabricatorNotificationStoryView' => 'applications/notification/view/PhabricatorNotificationStoryView.php', ··· 1698 1699 'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController', 1699 1700 'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine', 1700 1701 'PhabricatorNotificationController' => 'PhabricatorController', 1702 + 'PhabricatorNotificationIndividualController' => 'PhabricatorNotificationController', 1701 1703 'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController', 1702 1704 'PhabricatorNotificationStoryView' => 'PhabricatorNotificationView', 1703 1705 'PhabricatorNotificationTestController' => 'PhabricatorNotificationController',
+2
src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
··· 423 423 424 424 '/notification/test/' => 'PhabricatorNotificationTestController', 425 425 '/notification/panel/' => 'PhabricatorNotificationPanelController', 426 + '/notification/individual/' 427 + => 'PhabricatorNotificationIndividualController', 426 428 '/flag/' => array( 427 429 '' => 'PhabricatorFlagListController', 428 430 'view/(?P<view>[^/]+)/' => 'PhabricatorFlagListController',
+12
src/applications/feed/PhabricatorFeedStoryPublisher.php
··· 98 98 99 99 if (PhabricatorEnv::getEnvConfig('notification.enabled')) { 100 100 $this->insertNotifications($chrono_key); 101 + $this->sendNotification($chrono_key); 101 102 } 102 103 return $story; 103 104 } ··· 136 137 implode(', ', $sql)); 137 138 } 138 139 140 + private function sendNotification($chrono_key) { 141 + $aphlict_url = 'http://127.0.0.1:22281/push?'; //TODO: make configurable 142 + $future = new HTTPFuture($aphlict_url, array( 143 + "key" => (string)$chrono_key, 144 + // TODO: fix. \r\n appears to be appended to the final value here. 145 + // this is a temporary workaround 146 + "nothing" => "", 147 + )); 148 + $future->setMethod('POST'); 149 + $future->resolve(); 150 + } 139 151 140 152 /** 141 153 * We generate a unique chronological key for each story type because we want
+43
src/applications/notification/controller/PhabricatorNotificationIndividualController.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorNotificationIndividualController 20 + extends PhabricatorNotificationController { 21 + 22 + public function processRequest() { 23 + $request = $this->getRequest(); 24 + $user = $request->getUser(); 25 + 26 + $chron_key = $request->getStr('key'); 27 + $story = id(new PhabricatorFeedStoryNotification()) 28 + ->loadOneWhere('userPHID = %s AND chronologicalKey = %s', 29 + $user->getPHID(), 30 + $chron_key); 31 + 32 + if ($story == null) { 33 + $json = array( "pertinent" => false ); 34 + } else { 35 + $json = array( 36 + "pertinent" => true, 37 + "primaryObjectPHID" => $story->getPrimaryObjectPHID(), 38 + ); 39 + } 40 + 41 + return id(new AphrontAjaxResponse())->setContent($json); 42 + } 43 + }
+8
src/applications/notification/controller/PhabricatorNotificationPanelController.php
··· 33 33 $builder = new PhabricatorNotificationBuilder($stories); 34 34 $notifications_view = $builder->buildView(); 35 35 36 + $num_unconsumed = 0; 37 + foreach ($stories as $story) { 38 + if (!$story->getHasViewed()) { 39 + $num_unconsumed++; 40 + } 41 + } 42 + 36 43 $json = array( 37 44 "content" => $stories ? 38 45 $notifications_view->render() : 39 46 "<b>You currently have no notifications<b>", 47 + "number" => $num_unconsumed, 40 48 ); 41 49 42 50 return id(new AphrontAjaxResponse())->setContent($json);
+11 -11
src/view/page/PhabricatorStandardPageView.php
··· 376 376 377 377 if (PhabricatorEnv::getEnvConfig('notification.enabled') && 378 378 $user->isLoggedIn()) { 379 + 379 380 $aphlict_object_id = 'aphlictswfobject'; 380 381 381 - $aphlict_content = phutil_render_tag( 382 - 'object', 382 + $server_uri = new PhutilURI(PhabricatorEnv::getURI('')); 383 + $server_domain = $server_uri->getDomain(); 384 + 385 + Javelin::initBehavior( 386 + 'aphlict-listen', 383 387 array( 384 - 'classid' => 'clsid:d27cdb6e-ae6d-11cf-96b8-444553540000', 385 - ), 386 - '<param name="movie" value="/rsrc/swf/aphlict.swf" />'. 387 - '<param name="allowScriptAccess" value="always" />'. 388 - '<param name="wmode" value="opaque" />'. 389 - '<embed src="/rsrc/swf/aphlict.swf" wmode="opaque" id="'. 390 - $aphlict_object_id.'"></embed>'); 388 + 'id' => $aphlict_object_id, 389 + 'server' => $server_domain, 390 + 'port' => 2600, 391 + )); 391 392 392 393 Javelin::initBehavior('aphlict-dropdown', array()); 393 394 ··· 405 406 $notification_header = 406 407 $notification_indicator. 407 408 '<td>'. 408 - '<div style="height:1px; width:1px;">'. 409 - $aphlict_content. 409 + '<div id="aphlictswf-container" style="height:1px; width:1px;">'. 410 410 '</div>'. 411 411 '</td>'; 412 412 $notification_dropdown =
+4 -75
support/aphlict/client/src/Aphlict.as
··· 7 7 import flash.events.*; 8 8 import flash.external.ExternalInterface; 9 9 10 - import com.phabricator.*; 11 - 12 10 import vegas.strings.JSON; 13 11 14 12 public class Aphlict extends Sprite { 15 13 16 14 private var client:String; 17 15 18 - private var master:LocalConnection; 19 - private var recv:LocalConnection; 20 - private var send:LocalConnection; 21 - 22 - private var receiver:AphlictReceiver; 23 - private var loyalUntil:Number = 0; 24 - private var subjects:Array; 25 - private var frequency:Number = 100; 26 - 27 16 private var socket:Socket; 28 17 private var readBuffer:ByteArray; 29 18 ··· 47 36 this.remoteServer = server; 48 37 this.remotePort = port; 49 38 50 - this.master = null; 51 - this.receiver = new AphlictReceiver(this); 52 - this.subjects = []; 53 - 54 - this.send = new LocalConnection(); 55 - 56 - this.recv = new LocalConnection(); 57 - this.recv.client = this.receiver; 58 - for (var ii:Number = 0; ii < 32; ii++) { 59 - try { 60 - this.recv.connect('aphlict_subject_' + ii); 61 - this.client = 'aphlict_subject_' + ii; 62 - } catch (x:Error) { 63 - // Some other Aphlict client is holding that ID. 64 - } 65 - } 66 - 67 - if (!this.client) { 68 - // Too many clients open already, just exit. 69 - return; 70 - } 71 - 72 - this.usurp(); 39 + this.connectToServer(); 40 + return; 73 41 } 74 42 75 - private function usurp():void { 76 - if (this.master) { 77 - for (var ii:Number = 0; ii < this.subjects.length; ii++) { 78 - if (this.subjects[ii] == this.client) { 79 - continue; 80 - } 81 - this.send.send(this.subjects[ii], 'remainLoyal'); 82 - } 83 - } else if (this.loyalUntil < new Date().getTime()) { 84 - var recv:LocalConnection = new LocalConnection(); 85 - recv.client = this.receiver; 86 - try { 87 - recv.connect('aphlict_master'); 88 - this.master = recv; 89 - this.subjects = [this.client]; 90 - 91 - this.connectToServer(); 92 - 93 - } catch (x:Error) { 94 - // Can't become the master. 95 - } 96 - 97 - if (!this.master) { 98 - this.send.send('aphlict_master', 'becomeLoyal', this.client); 99 - this.remainLoyal(); 100 - } 101 - } 102 - setTimeout(this.usurp, this.frequency); 103 - } 104 43 105 44 public function connectToServer():void { 106 45 var socket:Socket = new Socket(); ··· 156 95 t.writeBytes(b, msg_len + 8); 157 96 this.readBuffer = t; 158 97 159 - for (var ii:Number = 0; ii < this.subjects.length; ii++) { 160 - this.send.send(this.subjects[ii], 'receiveMessage', data); 161 - } 98 + this.receiveMessage(data); 162 99 } else { 163 100 break; 164 101 } ··· 166 103 167 104 } 168 105 169 - public function remainLoyal():void { 170 - this.loyalUntil = new Date().getTime() + (2 * this.frequency); 171 - } 172 - 173 - public function becomeLoyal(subject:String):void { 174 - this.subjects.push(subject); 175 - } 176 - 177 106 public function receiveMessage(msg:Object):void { 178 107 this.externalInvoke('receive', msg); 179 108 } ··· 188 117 189 118 } 190 119 191 - } 120 + }
-25
support/aphlict/client/src/com/phabricator/AphlictReceiver.as
··· 1 - package com.phabricator { 2 - 3 - public class AphlictReceiver { 4 - 5 - private var core:Object; 6 - 7 - public function AphlictReceiver(core:Object) { 8 - this.core = core; 9 - } 10 - 11 - public function remainLoyal():void { 12 - this.core.remainLoyal(); 13 - } 14 - 15 - public function becomeLoyal(subject:String):void { 16 - this.core.becomeLoyal(subject); 17 - } 18 - 19 - public function receiveMessage(msg:Object):void { 20 - this.core.receiveMessage(msg); 21 - } 22 - 23 - } 24 - 25 - }
+109 -14
support/aphlict/server/aphlict_server.js
··· 1 1 var net = require('net'); 2 + var http = require('http'); 3 + var url = require('url'); 4 + var querystring = require('querystring'); 5 + var fs = require('fs'); 6 + 7 + // set up log file 8 + logfile = fs.createWriteStream('/var/log/aphlict.log', 9 + { flags: 'a', 10 + encoding: null, 11 + mode: 0666 }); 12 + logfile.write('----- ' + (new Date()).toLocaleString() + ' -----\n'); 13 + 14 + function log(str) { 15 + console.log(str); 16 + logfile.write(str + '\n'); 17 + } 18 + 2 19 3 20 function getFlashPolicy() { 4 21 return [ ··· 8 25 '<cross-domain-policy>', 9 26 '<allow-access-from domain="*" to-ports="2600"/>', 10 27 '</cross-domain-policy>' 11 - ].join("\n"); 28 + ].join('\n'); 12 29 } 13 30 14 31 net.createServer(function(socket) { 15 32 socket.on('data', function() { 16 33 socket.write(getFlashPolicy() + '\0'); 17 34 }); 35 + 36 + socket.on('error', function (e) { 37 + log('Error in policy server: ' + e); 38 + }); 18 39 }).listen(843); 19 40 20 - var sp_server = net.createServer(function(socket) { 21 - function xwrite() { 22 - var data = {hi: "hello"}; 23 - var serial = JSON.stringify(data); 24 41 25 - var length = Buffer.byteLength(serial, 'utf8'); 26 - length = length.toString(); 27 - while (length.length < 8) { 28 - length = "0" + length; 29 - } 42 + 43 + function write_json(socket, data) { 44 + var serial = JSON.stringify(data); 45 + var length = Buffer.byteLength(serial, 'utf8'); 46 + length = length.toString(); 47 + while (length.length < 8) { 48 + length = '0' + length; 49 + } 50 + socket.write(length + serial); 51 + } 52 + 53 + 54 + var clients = {}; 55 + var current_connections = 0; 56 + // According to the internet up to 2^53 can 57 + // be stored in javascript, this is less than that 58 + var MAX_ID = 9007199254740991;//2^53 -1 30 59 31 - socket.write(length + serial); 60 + // If we get one connections per millisecond this will 61 + // be fine as long as someone doesn't maintain a 62 + // connection for longer than 6854793 years. If 63 + // you want to write something pretty be my guest 32 64 33 - console.log('write : ' + length + serial); 65 + function generate_id() { 66 + if (typeof generate_id.current_id == 'undefined' 67 + || generate_id.current_id > MAX_ID) { 68 + generate_id.current_id = 0; 34 69 } 70 + return generate_id.current_id++; 71 + } 72 + 73 + var send_server = net.createServer(function(socket) { 74 + var client_id = generate_id(); 35 75 36 76 socket.on('connect', function() { 77 + clients[client_id] = socket; 78 + current_connections++; 79 + log(client_id + ': connected\t\t(' 80 + + current_connections + ' current connections)'); 81 + }); 37 82 38 - xwrite(); 39 - setInterval(xwrite, 1000); 83 + socket.on('close', function() { 84 + delete clients[client_id]; 85 + current_connections--; 86 + log(client_id + ': closed\t\t(' 87 + + current_connections + ' current connections)'); 88 + }); 89 + 90 + socket.on('timeout', function() { 91 + log(client_id + ': timed out!'); 92 + }); 40 93 94 + socket.on('end', function() { 95 + log(client_id + ': ended the connection'); 96 + // node automatically closes half-open connections 97 + }); 98 + 99 + socket.on('error', function (e) { 100 + console.log('Uncaught error in send server: ' + e); 41 101 }); 42 102 }).listen(2600); 103 + 104 + 105 + 106 + var receive_server = http.createServer(function(request, response) { 107 + response.writeHead(200, {'Content-Type' : 'text/plain'}); 108 + 109 + if (request.method == 'POST') { // Only pay attention to POST requests 110 + var body = ''; 111 + 112 + request.on('data', function (data) { 113 + body += data; 114 + }); 115 + 116 + request.on('end', function () { 117 + var data = querystring.parse(body); 118 + log('notification: ' + JSON.stringify(data)); 119 + broadcast(data); 120 + response.end(); 121 + }); 122 + } 123 + }).listen(22281, '127.0.0.1'); 124 + 125 + function broadcast(data) { 126 + for(var client_id in clients) { 127 + try { 128 + write_json(clients[client_id], data); 129 + log(' wrote to client ' + client_id); 130 + } catch (error) { 131 + delete clients[client_id]; 132 + current_connections--; 133 + log(' ERROR: could not write to client ' + client_id); 134 + } 135 + } 136 + } 137 +
+20 -4
webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js
··· 11 11 var dropdown = JX.$('phabricator-notification-dropdown'); 12 12 var indicator = JX.$('phabricator-notification-indicator'); 13 13 var visible = false; 14 + var request = null; 14 15 15 - //populate panel 16 - (new JX.Request('/notification/panel/', 17 - function(response) { 16 + function refresh() { 17 + if (request) { //already fetching 18 + return; 19 + } 20 + 21 + request = new JX.Request('/notification/panel/', function(response) { 22 + indicator.textContent = '' + response.number; 23 + if (response.number == 0) { 24 + indicator.style.fontWeight = ""; 25 + } else { 26 + indicator.style.fontWeight = "bold"; 27 + } 18 28 JX.DOM.setContent(dropdown, JX.$H(response.content)); 19 - })).send(); 29 + request = null; 30 + }); 31 + request.send(); 32 + } 20 33 34 + //populate panel 35 + refresh(); 21 36 22 37 JX.Stratcom.listen( 23 38 'click', ··· 48 63 } 49 64 ) 50 65 66 + JX.Stratcom.listen('notification-panel-update', null, refresh); 51 67 });
+20 -6
webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js
··· 4 4 * javelin-aphlict 5 5 * javelin-util 6 6 * javelin-stratcom 7 + * javelin-behavior-aphlict-dropdown 7 8 */ 8 9 9 10 JX.behavior('aphlict-listen', function(config) { 10 11 function onready() { 11 - JX.log("The flash component is ready!"); 12 - 13 12 var client = new JX.Aphlict(config.id, config.server, config.port) 14 13 .setHandler(function(type, message) { 15 14 if (message) { 16 - JX.log("Got aphlict event '" + type + "':"); 17 - JX.log(message); 18 - } else { 19 - JX.log("Got aphlict event '" + type + "'."); 15 + if (type == 'receive') { 16 + var request = new JX.Request('/notification/individual/', 17 + function(response) { 18 + if (response.pertinent) { 19 + JX.Stratcom.invoke('notification-panel-update', null, {}); 20 + } 21 + }); 22 + request.addData({ "key": message.key }); 23 + request.send(); 24 + } 20 25 } 21 26 }) 22 27 .start(); ··· 27 32 // If we just go crazy and start making calls to it before it loads, its 28 33 // interfaces won't be registered yet. 29 34 JX.Stratcom.listen('aphlict-component-ready', null, onready); 35 + 36 + // Add Flash object to page 37 + JX.$("aphlictswf-container").innerHTML = 38 + '<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000">' 39 + + '<param name="movie" value="/rsrc/swf/aphlict.swf" />' 40 + + '<param name="allowScriptAccess" value="always" />' 41 + + '<param name="wmode" value="opaque" />' 42 + + '<embed src="/rsrc/swf/aphlict.swf" wmode="opaque" id="aphlictswfobject">' 43 + + '</embed></object>'; //Evan sanctioned 30 44 });
webroot/rsrc/swf/aphlict.swf

This is a binary file and will not be displayed.