├── .gitignore ├── README.md ├── pom.xml └── src ├── main ├── deploy │ └── package │ │ ├── macosx │ │ ├── Frameworks │ │ │ └── Growl.framework │ │ │ │ ├── Growl │ │ │ │ ├── Headers │ │ │ │ ├── Growl.h │ │ │ │ ├── GrowlApplicationBridge.h │ │ │ │ └── GrowlDefines.h │ │ │ │ ├── Resources │ │ │ │ └── Info.plist │ │ │ │ └── Versions │ │ │ │ ├── A │ │ │ │ ├── Growl │ │ │ │ ├── Headers │ │ │ │ │ ├── Growl.h │ │ │ │ │ ├── GrowlApplicationBridge.h │ │ │ │ │ └── GrowlDefines.h │ │ │ │ ├── Resources │ │ │ │ │ └── Info.plist │ │ │ │ └── _CodeSignature │ │ │ │ │ └── CodeResources │ │ │ │ └── Current │ │ │ │ ├── Growl │ │ │ │ ├── Headers │ │ │ │ ├── Growl.h │ │ │ │ ├── GrowlApplicationBridge.h │ │ │ │ └── GrowlDefines.h │ │ │ │ ├── Resources │ │ │ │ └── Info.plist │ │ │ │ └── _CodeSignature │ │ │ │ └── CodeResources │ │ ├── Info.plist │ │ ├── MacOS │ │ │ ├── .DS_Store │ │ │ └── MacGap │ │ ├── Resources │ │ │ └── en.lproj │ │ │ │ ├── Credits.rtf │ │ │ │ ├── InfoPlist.strings │ │ │ │ ├── MainMenu.nib │ │ │ │ └── Window.nib │ │ ├── trsst-client-0.2-SNAPSHOT-background.png │ │ ├── trsst-client-0.2-SNAPSHOT-post-image.sh │ │ ├── trsst-client-0.2-SNAPSHOT-volume.icns │ │ └── trsst-client-0.2-SNAPSHOT.icns │ │ └── windows │ │ └── trsst-client-0.2-SNAPSHOT.ico ├── java │ └── com │ │ └── trsst │ │ ├── Command.java │ │ ├── Common.java │ │ ├── Crypto.java │ │ ├── client │ │ ├── AnonymSSLSocketFactory.java │ │ ├── Client.java │ │ ├── EntryOptions.java │ │ ├── FeedOptions.java │ │ └── MultiPartRequestEntity.java │ │ ├── server │ │ ├── AbderaProvider.java │ │ ├── AbstractMultipartAdapter.java │ │ ├── CachingStorage.java │ │ ├── FileStorage.java │ │ ├── HomeAdapter.java │ │ ├── LuceneStorage.java │ │ ├── Server.java │ │ ├── Storage.java │ │ └── TrsstAdapter.java │ │ └── ui │ │ ├── AppMain.java │ │ ├── AppServlet.java │ │ └── AppleEvents.java └── resources │ ├── com │ └── trsst │ │ └── ui │ │ └── site │ │ ├── boiler.css │ │ ├── composer.js │ │ ├── controller.js │ │ ├── favicon.ico │ │ ├── icon-256.png │ │ ├── icon-back.png │ │ ├── icon-rss.png │ │ ├── index.html │ │ ├── jquery-1.10.1.js │ │ ├── loading-on-gray.gif │ │ ├── loading-on-orange.gif │ │ ├── loading-on-white.gif │ │ ├── model.js │ │ ├── note.png │ │ ├── note.svg │ │ ├── pollster.js │ │ ├── renderer.js │ │ └── ui.css │ └── eawt.jar └── test └── java └── com └── trsst └── TrsstTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .classpath 3 | 4 | .project 5 | 6 | *.prefs 7 | 8 | dependency-reduced-pom.xml 9 | /target 10 | /target 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | trsst 2 | ===== 3 | 4 | What is trsst? 5 | ------------------------------------ 6 | 7 | It depends on who you are. 8 | 9 | - For most users, trsst looks like a microblogging social network -- a twitter clone -- where you can follow other people and news feeds. 10 | 11 | - For other users, trsst looks like a stream-style RSS reader with built-in microblog publishing capabilities. 12 | 13 | - And for a few, trsst looks like an extension to the Atom Publishing Protocol where anyone can anonymously create self-signed and/or self-encrypted feeds and entries and publish them to any participating server. 14 | 15 | All of these are correct. 16 | 17 | 18 | Download Alpha v0.2 19 | ------------------------------------ 20 | 21 | Current binaries are downloadable here: 22 | https://github.com/TrsstProject/trsst/releases 23 | 24 | 25 | License 26 | ------------------------------------ 27 | 28 | All Trsst Project source code is licensed under the Apache License, Version 2.0. 29 | http://www.apache.org/licenses/LICENSE-2.0 30 | 31 | 32 | Frequently Asked Questions 33 | ------------------------------------ 34 | 35 | The FAQ is currently kept here: 36 | https://github.com/TrsstProject/trsst/wiki/Frequently-Asked-Questions 37 | 38 | 39 | Development Howto 40 | ------------------------------------ 41 | 42 | Requires Java and Maven. 43 | 44 | To build: 45 | 46 | mvn clean install 47 | 48 | To run: 49 | 50 | java -jar target/trsst-client-0.2-SNAPSHOT-exe.jar 51 | 52 | Usage: 53 | 54 | post [] [--status ] [--encrypt ] 55 | -a,--attach Attach the specified file, or - for std input 56 | -b,--base Set base URL for this feed 57 | -c,--content Specify entry content on command line 58 | -e,--encrypt Encrypt entry for specified public key 59 | -g,--tag Add a tag 60 | -i,--icon Set as this feed's icon or specify url 61 | -l,--logo Set as this feed's logo or specify url 62 | -m,--mail Set this feed's author email 63 | -n,--name Set this feed's author name 64 | -p,--pass Specify passphrase on the command line 65 | -r,--mention Add a mention 66 | -s,--status Specify status update on command line 67 | --strict Require SSL certs 68 | --subtitle Set this feed's subtitle 69 | -t,--title Set this feed's title 70 | -u,--url Attach the specified url to the new entry 71 | -v,--verb Specify an activitystreams verb for this entry 72 | --vanity Generate feed id with specified prefix 73 | 74 | pull ... 75 | -d,--decrypt Decrypt entries as specified recipient id 76 | -h,--host Set host server for this operation 77 | 78 | push ... 79 | -d,--decrypt Decrypt entries as specified recipient id 80 | -h,--host Set host server for this operation 81 | 82 | serve 83 | --api Expose client API 84 | --clear Turn off SSL 85 | --gui Launch embedded GUI 86 | --port Specify port 87 | --tor Use TOR (experimental) 88 | 89 | 90 | Example: start a server. 91 | 92 | $ java -jar target/trsst-client-0.2-SNAPSHOT-exe.jar serve --port 8181 93 | 94 | Services now available at: https://192.168.1.5:8181/feed 95 | 96 | Example: create a new empty feed. 97 | 98 | $ java -jar target/trsst-client-0.2-SNAPSHOT-exe.jar post 99 | 100 | Starting temporary service at: http://192.168.1.5:51341 101 | Generating new feed id... 102 | New feed id created: M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1 103 | 104 | 105 | 2014-03-10T20:34:13.440Z 106 | 107 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEvzJUjrJSRS0NHsKq1yHur5xdhH35ao4IVTDF_WuwZti3AiAt2gZ8Sehp83PV8yD9ONlw5-DiXYbgY5PUgJTVcQ 108 | 109 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE9FJDVs846H1ne1G5upDY8CIgF_HhmCILl-967JRQTjYTzRVwHMz5mFakwOKdYBcea9Q_1wLL1L-nqWznUh_uQg 110 | urn:feed:M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | GK1D1kLSydLFRXCHJnPk2p9LjUQ= 123 | 124 | 125 | 126 | CGjdfB+n6uAjIDQ8UCp+GMyjlP4OazvIwzoHKZ2FL8dRGK7PVfLq/dPaGt2SysHHlmCKNhmvQ2BE 127 | 2+hQAUtQ7g== 128 | 129 | 130 | 131 | Example: create a new post on a preexisting feed. 132 | 133 | $ java -jar target/trsst-client-0.2-SNAPSHOT-exe.jar post --status "First Post" M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1 134 | 135 | Starting temporary service at: http://192.168.1.5:51371 136 | Obtaining keys for feed id: M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1 137 | Using existing account id: M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1 138 | 139 | 140 | 2014-03-10T20:35:03.379Z 141 | urn:feed:M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1 142 | 143 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEvzJUjrJSRS0NHsKq1yHur5xdhH35ao4IVTDF_WuwZti3AiAt2gZ8Sehp83PV8yD9ONlw5-DiXYbgY5PUgJTVcQ 144 | 145 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE9FJDVs846H1ne1G5upDY8CIgF_HhmCILl-967JRQTjYTzRVwHMz5mFakwOKdYBcea9Q_1wLL1L-nqWznUh_uQg 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | n/4ocrBhhFr/9+uIzmKkGQ0ufbk= 158 | 159 | 160 | 161 | 68l6RxoG8ZAs3QvRs3HNeQipHE/QYuRD9jhqfZzoJO2PzfdCJ9DU2/XdDEwFBJzp96fJjW/fmQWI 162 | VEuQa+GBQw== 163 | 164 | 165 | 2014-03-10T20:35:03.379Z 166 | 167 | urn:entry:M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1:144adb4ae53 168 | 2014-03-10T20:35:03.379Z 169 | First Post 170 | attribution, no derivatives, revoked if 171 | deleted 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | fghpP3mG0nzB5Sj4kyqlNbRGsDY= 184 | 185 | 186 | 187 | ys+J0FryCApGD/juC20q9YrbVTIH5wQqmhgvuFmYZdBlhEVpIUg6XaFNbjc4eiAnxMs5r1qACp9n 188 | NB1GrL7MuQ== 189 | 190 | 191 | 192 | 193 | Example: create an encrypted post on a preexisting feed. 194 | 195 | $ java -jar target/trsst-client-0.2-SNAPSHOT-exe.jar post --status "Secret Post" M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1 196 | -- encrypt MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEQn1eOiToQlurH0gIE0AsBTJNBF1jrOSSIr8i4RSRdvx7dtkD1hre0vgPabJMLH9QktK6AYhl31xkf3xqp_mPxw 197 | 198 | Starting temporary service at: http://192.168.1.5:51384 199 | Obtaining keys for feed id: M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1 200 | Using existing account id: M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1 201 | 202 | 203 | 2014-03-10T20:37:10.244Z 204 | urn:feed:M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1 205 | 206 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEvzJUjrJSRS0NHsKq1yHur5xdhH35ao4IVTDF_WuwZti3AiAt2gZ8Sehp83PV8yD9ONlw5-DiXYbgY5PUgJTVcQ 207 | 208 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE9FJDVs846H1ne1G5upDY8CIgF_HhmCILl-967JRQTjYTzRVwHMz5mFakwOKdYBcea9Q_1wLL1L-nqWznUh_uQg 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 5KBNuLZ9TTbN0UL65hFSjLN+myE= 221 | 222 | 223 | 224 | aCS1AhKl29Pdlcgs6SRhI6oNhEWpak1/8Ft8CrgwTF/qjh7Bcy3H7UjiBC97YI2bHZcedV5Z0BRf 225 | htGt6UCGUQ== 226 | 227 | 228 | 229 | urn:entry:M4QovUyLYdyMy2ZB4s7BwLztwYcyJqrJ1:144adb69de4 230 | 2014-03-10T20:37:10.244Z 231 | 2014-03-10T20:37:10.244Z 232 | 233 | ys+J0FryCApGD/juC20q9YrbVTIH5wQqmhgvuFmYZdBlhEVpIUg6XaFNbjc4eiAnxMs5r1qACp9n 234 | NB1GrL7MuQ== 235 | Encrypted content 236 | 237 | 238 | 239 | 240 | BO8MZ3_KSnwnTD3g0apIsP6GnmzF4RrXMhVXV7VgcqBYuITqfAcKwR6s0rjb0BBGRrtxuiiqz6dIDbRGcTSuxVdIgTPcWMvDsixO-I6wnQTT5KXl6_DhoIO_perg9ap0cT-dTi6qS-mKdaN0ADqf3E6vTsgpGTt3h_8mNQCYo9baJbobFgdmzjRi75KIMyXijA 241 | 242 | 243 | 244 | 245 | 246 | BAr8zcyj6SrI0T70_-U44V4edUxB9jNNaNLfFumWrL-yj_DoOm-DcDy4lwW3SnjqeFb-_at-72bb-mPrmaMnbAk6CVVfHDgg4pRDTriBQiCxwX8VDlCA5ileCdX0qi40c2NTgsEGS7IWwmnHwfIGj5_C7bmeyiIEhB9PiU_v7t-KRXrNPblcNarZ3Gbsu71SJw 247 | 248 | 249 | 250 | 251 | 252 | nxTxeHPGnONdAEvpWtClWbA7C5SQsAdjFeleXDnMrOPb8YqTuAy_Uc4d39ycT7aoMmZlH1VWPfWyjJq43azenQbtmluNaw1ABxRvI8yWfNJZYb6fIxDQLL4en_bQ9GW5Z2epJuPU0IxU4gj99YZ_hOjyO5jpOnUO7shoTt8CRvRdtoEqI8QGT8-nmJyIg4wNMeLRTEcQ503dekG8ks6TrFGkrYAa5nlDMxrlAe_2etfr1eBDctgHkk18gCFTjO18Ydjx955DzsbZHqmnqok4eom6SNVdkHXWjQMt4bhWgJWFDjnwpaYoiqcms8upDNope6G98sib8vNX6Muw8liEq787xy4LMLbS6fbMPHzHDYV7yZV1xKA4YRKV99mWkENnhmjGrNjMrV5WVmBg_8VU2h4FicGkYtBl3mPD_Q967-CyTJlwdNWmtrECascazcdSROI7fpB0ZDnE9gdNPUlUC6YACXemjYRCY1OFyOwiVBFX8UhqR9CLsb0KzQtC_WOavNpGneZoPEO3hGVyQusawYb4ESZumuDPLSYkm6TTiRBxxlgCgmWu45zBNidOmDy2ARzq_f-aEozbKyzDJEy7hR-jXWbRrxvVUV24eiJd0gV8c7AEJYT6edXnO2PXbEjR3Itl-JOBec4k8Y2dTkUDXvdO1u1hk6uZ_oD1-RizQVo 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 1Izt6hPxvZExmTWiSPT5QbMuoKs= 268 | 269 | 270 | 271 | iUFSwxDvxti1hjDFgmux7NPrk3PEEFAnhNnNqlad2VB42iir2Xl8RPcEZA1Sne7Rc51b376E9Iy0 272 | qT5shelegQ== 273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Growl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/macosx/Frameworks/Growl.framework/Growl -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Headers/Growl.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifdef __OBJC__ 4 | # include 5 | #endif 6 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Headers/GrowlDefines.h: -------------------------------------------------------------------------------- 1 | // 2 | // GrowlDefines.h 3 | // 4 | 5 | #ifndef _GROWLDEFINES_H 6 | #define _GROWLDEFINES_H 7 | 8 | #ifdef __OBJC__ 9 | #define XSTR(x) (@x) 10 | #else 11 | #define XSTR CFSTR 12 | #endif 13 | 14 | /*! @header GrowlDefines.h 15 | * @abstract Defines all the notification keys. 16 | * @discussion Defines all the keys used for registration with Growl and for 17 | * Growl notifications. 18 | * 19 | * Most applications should use the functions or methods of Growl.framework 20 | * instead of posting notifications such as those described here. 21 | * @updated 2004-01-25 22 | */ 23 | 24 | // UserInfo Keys for Registration 25 | #pragma mark UserInfo Keys for Registration 26 | 27 | /*! @group Registration userInfo keys */ 28 | /* @abstract Keys for the userInfo dictionary of a GROWL_APP_REGISTRATION distributed notification. 29 | * @discussion The values of these keys describe the application and the 30 | * notifications it may post. 31 | * 32 | * Your application must register with Growl before it can post Growl 33 | * notifications (and have them not be ignored). However, as of Growl 0.6, 34 | * posting GROWL_APP_REGISTRATION notifications directly is no longer the 35 | * preferred way to register your application. Your application should instead 36 | * use Growl.framework's delegate system. 37 | * See +[GrowlApplicationBridge setGrowlDelegate:] or Growl_SetDelegate for 38 | * more information. 39 | */ 40 | 41 | /*! @defined GROWL_APP_NAME 42 | * @abstract The name of your application. 43 | * @discussion The name of your application. This should remain stable between 44 | * different versions and incarnations of your application. 45 | * For example, "SurfWriter" is a good app name, whereas "SurfWriter 2.0" and 46 | * "SurfWriter Lite" are not. 47 | */ 48 | #define GROWL_APP_NAME XSTR("ApplicationName") 49 | /*! @defined GROWL_APP_ID 50 | * @abstract The bundle identifier of your application. 51 | * @discussion The bundle identifier of your application. This key should 52 | * be unique for your application while there may be several applications 53 | * with the same GROWL_APP_NAME. 54 | * This key is optional. 55 | */ 56 | #define GROWL_APP_ID XSTR("ApplicationId") 57 | /*! @defined GROWL_APP_ICON_DATA 58 | * @abstract The image data for your application's icon. 59 | * @discussion Image data representing your application's icon. This may be 60 | * superimposed on a notification icon as a badge, used as the notification 61 | * icon when a notification-specific icon is not supplied, or ignored 62 | * altogether, depending on the display. Must be in a format supported by 63 | * NSImage, such as TIFF, PNG, GIF, JPEG, BMP, PICT, or PDF. 64 | * 65 | * Optional. Not supported by all display plugins. 66 | */ 67 | #define GROWL_APP_ICON_DATA XSTR("ApplicationIcon") 68 | /*! @defined GROWL_NOTIFICATIONS_DEFAULT 69 | * @abstract The array of notifications to turn on by default. 70 | * @discussion These are the names of the notifications that should be enabled 71 | * by default when your application registers for the first time. If your 72 | * application reregisters, Growl will look here for any new notification 73 | * names found in GROWL_NOTIFICATIONS_ALL, but ignore any others. 74 | */ 75 | #define GROWL_NOTIFICATIONS_DEFAULT XSTR("DefaultNotifications") 76 | /*! @defined GROWL_NOTIFICATIONS_ALL 77 | * @abstract The array of all notifications your application can send. 78 | * @discussion These are the names of all of the notifications that your 79 | * application may post. See GROWL_NOTIFICATION_NAME for a discussion of good 80 | * notification names. 81 | */ 82 | #define GROWL_NOTIFICATIONS_ALL XSTR("AllNotifications") 83 | /*! @defined GROWL_NOTIFICATIONS_HUMAN_READABLE_DESCRIPTIONS 84 | * @abstract A dictionary of human-readable names for your notifications. 85 | * @discussion By default, the Growl UI will display notifications by the names given in GROWL_NOTIFICATIONS_ALL 86 | * which correspond to the GROWL_NOTIFICATION_NAME. This dictionary specifies the human-readable name to display. 87 | * The keys of the dictionary are GROWL_NOTIFICATION_NAME strings; the objects are the human-readable versions. 88 | * For any GROWL_NOTIFICATION_NAME not specific in this dictionary, the GROWL_NOTIFICATION_NAME will be displayed. 89 | * 90 | * This key is optional. 91 | */ 92 | #define GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES XSTR("HumanReadableNames") 93 | /*! @defined GROWL_NOTIFICATIONS_DESCRIPTIONS 94 | * @abstract A dictionary of descriptions of _when_ each notification occurs 95 | * @discussion This is an NSDictionary whose keys are GROWL_NOTIFICATION_NAME strings and whose objects are 96 | * descriptions of _when_ each notification occurs, such as "You received a new mail message" or 97 | * "A file finished downloading". 98 | * 99 | * This key is optional. 100 | */ 101 | #define GROWL_NOTIFICATIONS_DESCRIPTIONS XSTR("NotificationDescriptions") 102 | 103 | /*! @defined GROWL_TICKET_VERSION 104 | * @abstract The version of your registration ticket. 105 | * @discussion Include this key in a ticket plist file that you put in your 106 | * application bundle for auto-discovery. The current ticket version is 1. 107 | */ 108 | #define GROWL_TICKET_VERSION XSTR("TicketVersion") 109 | // UserInfo Keys for Notifications 110 | #pragma mark UserInfo Keys for Notifications 111 | 112 | /*! @group Notification userInfo keys */ 113 | /* @abstract Keys for the userInfo dictionary of a GROWL_NOTIFICATION distributed notification. 114 | * @discussion The values of these keys describe the content of a Growl 115 | * notification. 116 | * 117 | * Not all of these keys are supported by all displays. Only the name, title, 118 | * and description of a notification are universal. Most of the built-in 119 | * displays do support all of these keys, and most other visual displays 120 | * probably will also. But, as of 0.6, the Log, MailMe, and Speech displays 121 | * support only textual data. 122 | */ 123 | 124 | /*! @defined GROWL_NOTIFICATION_NAME 125 | * @abstract The name of the notification. 126 | * @discussion The name of the notification. Note that if you do not define 127 | * GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES when registering your ticket originally this name 128 | * will the one displayed within the Growl preference pane and should be human-readable. 129 | */ 130 | #define GROWL_NOTIFICATION_NAME XSTR("NotificationName") 131 | /*! @defined GROWL_NOTIFICATION_TITLE 132 | * @abstract The title to display in the notification. 133 | * @discussion The title of the notification. Should be very brief. 134 | * The title usually says what happened, e.g. "Download complete". 135 | */ 136 | #define GROWL_NOTIFICATION_TITLE XSTR("NotificationTitle") 137 | /*! @defined GROWL_NOTIFICATION_DESCRIPTION 138 | * @abstract The description to display in the notification. 139 | * @discussion The description should be longer and more verbose than the title. 140 | * The description usually tells the subject of the action, 141 | * e.g. "Growl-0.6.dmg downloaded in 5.02 minutes". 142 | */ 143 | #define GROWL_NOTIFICATION_DESCRIPTION XSTR("NotificationDescription") 144 | /*! @defined GROWL_NOTIFICATION_ICON 145 | * @discussion Image data for the notification icon. Image data must be in a format 146 | * supported by NSImage, such as TIFF, PNG, GIF, JPEG, BMP, PICT, or PDF. 147 | * 148 | * Optional. Not supported by all display plugins. 149 | */ 150 | #define GROWL_NOTIFICATION_ICON_DATA XSTR("NotificationIcon") 151 | /*! @defined GROWL_NOTIFICATION_APP_ICON 152 | * @discussion Image data for the application icon, in case GROWL_APP_ICON does 153 | * not apply for some reason. Image data be in a format supported by NSImage, such 154 | * as TIFF, PNG, GIF, JPEG, BMP, PICT, or PDF. 155 | * 156 | * Optional. Not supported by all display plugins. 157 | */ 158 | #define GROWL_NOTIFICATION_APP_ICON_DATA XSTR("NotificationAppIcon") 159 | /*! @defined GROWL_NOTIFICATION_PRIORITY 160 | * @discussion The priority of the notification as an integer number from 161 | * -2 to +2 (+2 being highest). 162 | * 163 | * Optional. Not supported by all display plugins. 164 | */ 165 | #define GROWL_NOTIFICATION_PRIORITY XSTR("NotificationPriority") 166 | /*! @defined GROWL_NOTIFICATION_STICKY 167 | * @discussion A Boolean number controlling whether the notification is sticky. 168 | * 169 | * Optional. Not supported by all display plugins. 170 | */ 171 | #define GROWL_NOTIFICATION_STICKY XSTR("NotificationSticky") 172 | /*! @defined GROWL_NOTIFICATION_CLICK_CONTEXT 173 | * @abstract Identifies which notification was clicked. 174 | * @discussion An identifier for the notification for clicking purposes. 175 | * 176 | * This will be passed back to the application when the notification is 177 | * clicked. It must be plist-encodable (a data, dictionary, array, number, or 178 | * string object), and it should be unique for each notification you post. 179 | * A good click context would be a UUID string returned by NSProcessInfo or 180 | * CFUUID. 181 | * 182 | * Optional. Not supported by all display plugins. 183 | */ 184 | #define GROWL_NOTIFICATION_CLICK_CONTEXT XSTR("NotificationClickContext") 185 | 186 | /*! @defined GROWL_NOTIFICATION_IDENTIFIER 187 | * @abstract An identifier for the notification for coalescing purposes. 188 | * Notifications with the same identifier fall into the same class; only 189 | * the last notification of a class is displayed on the screen. If a 190 | * notification of the same class is currently being displayed, it is 191 | * replaced by this notification. 192 | * 193 | * Optional. Not supported by all display plugins. 194 | */ 195 | #define GROWL_NOTIFICATION_IDENTIFIER XSTR("GrowlNotificationIdentifier") 196 | 197 | /*! @defined GROWL_APP_PID 198 | * @abstract The process identifier of the process which sends this 199 | * notification. If this field is set, the application will only receive 200 | * clicked and timed out notifications which originate from this process. 201 | * 202 | * Optional. 203 | */ 204 | #define GROWL_APP_PID XSTR("ApplicationPID") 205 | 206 | /*! @defined GROWL_NOTIFICATION_PROGRESS 207 | * @abstract If this key is set, it should contain a double value wrapped 208 | * in a NSNumber which describes some sort of progress (from 0.0 to 100.0). 209 | * If this is key is not set, no progress bar is shown. 210 | * 211 | * Optional. Not supported by all display plugins. 212 | */ 213 | #define GROWL_NOTIFICATION_PROGRESS XSTR("NotificationProgress") 214 | 215 | // Notifications 216 | #pragma mark Notifications 217 | 218 | /*! @group Notification names */ 219 | /* @abstract Names of distributed notifications used by Growl. 220 | * @discussion These are notifications used by applications (directly or 221 | * indirectly) to interact with Growl, and by Growl for interaction between 222 | * its components. 223 | * 224 | * Most of these should no longer be used in Growl 0.6 and later, in favor of 225 | * Growl.framework's GrowlApplicationBridge APIs. 226 | */ 227 | 228 | /*! @defined GROWL_APP_REGISTRATION 229 | * @abstract The distributed notification for registering your application. 230 | * @discussion This is the name of the distributed notification that can be 231 | * used to register applications with Growl. 232 | * 233 | * The userInfo dictionary for this notification can contain these keys: 234 | *
    235 | *
  • GROWL_APP_NAME
  • 236 | *
  • GROWL_APP_ICON_DATA
  • 237 | *
  • GROWL_NOTIFICATIONS_ALL
  • 238 | *
  • GROWL_NOTIFICATIONS_DEFAULT
  • 239 | *
240 | * 241 | * No longer recommended as of Growl 0.6. An alternate method of registering 242 | * is to use Growl.framework's delegate system. 243 | * See +[GrowlApplicationBridge setGrowlDelegate:] or Growl_SetDelegate for 244 | * more information. 245 | */ 246 | #define GROWL_APP_REGISTRATION XSTR("GrowlApplicationRegistrationNotification") 247 | /*! @defined GROWL_APP_REGISTRATION_CONF 248 | * @abstract The distributed notification for confirming registration. 249 | * @discussion The name of the distributed notification sent to confirm the 250 | * registration. Used by the Growl preference pane. Your application probably 251 | * does not need to use this notification. 252 | */ 253 | #define GROWL_APP_REGISTRATION_CONF XSTR("GrowlApplicationRegistrationConfirmationNotification") 254 | /*! @defined GROWL_NOTIFICATION 255 | * @abstract The distributed notification for Growl notifications. 256 | * @discussion This is what it all comes down to. This is the name of the 257 | * distributed notification that your application posts to actually send a 258 | * Growl notification. 259 | * 260 | * The userInfo dictionary for this notification can contain these keys: 261 | *
    262 | *
  • GROWL_NOTIFICATION_NAME (required)
  • 263 | *
  • GROWL_NOTIFICATION_TITLE (required)
  • 264 | *
  • GROWL_NOTIFICATION_DESCRIPTION (required)
  • 265 | *
  • GROWL_NOTIFICATION_ICON
  • 266 | *
  • GROWL_NOTIFICATION_APP_ICON
  • 267 | *
  • GROWL_NOTIFICATION_PRIORITY
  • 268 | *
  • GROWL_NOTIFICATION_STICKY
  • 269 | *
  • GROWL_NOTIFICATION_CLICK_CONTEXT
  • 270 | *
  • GROWL_APP_NAME (required)
  • 271 | *
272 | * 273 | * No longer recommended as of Growl 0.6. Three alternate methods of posting 274 | * notifications are +[GrowlApplicationBridge notifyWithTitle:description:notificationName:iconData:priority:isSticky:clickContext:], 275 | * Growl_NotifyWithTitleDescriptionNameIconPriorityStickyClickContext, and 276 | * Growl_PostNotification. 277 | */ 278 | #define GROWL_NOTIFICATION XSTR("GrowlNotification") 279 | /*! @defined GROWL_PING 280 | * @abstract A distributed notification to check whether Growl is running. 281 | * @discussion This is used by the Growl preference pane. If it receives a 282 | * GROWL_PONG, the preference pane takes this to mean that Growl is running. 283 | */ 284 | #define GROWL_PING XSTR("Honey, Mind Taking Out The Trash") 285 | /*! @defined GROWL_PONG 286 | * @abstract The distributed notification sent in reply to GROWL_PING. 287 | * @discussion GrowlHelperApp posts this in reply to GROWL_PING. 288 | */ 289 | #define GROWL_PONG XSTR("What Do You Want From Me, Woman") 290 | /*! @defined GROWL_IS_READY 291 | * @abstract The distributed notification sent when Growl starts up. 292 | * @discussion GrowlHelperApp posts this when it has begin listening on all of 293 | * its sources for new notifications. GrowlApplicationBridge (in 294 | * Growl.framework), upon receiving this notification, reregisters using the 295 | * registration dictionary supplied by its delegate. 296 | */ 297 | #define GROWL_IS_READY XSTR("Lend Me Some Sugar; I Am Your Neighbor!") 298 | 299 | 300 | /*! @defined GROWL_DISTRIBUTED_NOTIFICATION_CLICKED_SUFFIX 301 | * @abstract Part of the name of the distributed notification sent when a supported notification is clicked. 302 | * @discussion When a Growl notification with a click context is clicked on by 303 | * the user, Growl posts a distributed notification whose name is in the format: 304 | * [NSString stringWithFormat:@"%@-%d-%@", appName, pid, GROWL_DISTRIBUTED_NOTIFICATION_CLICKED_SUFFIX] 305 | * The GrowlApplicationBridge responds to this notification by calling a callback in its delegate. 306 | */ 307 | #define GROWL_DISTRIBUTED_NOTIFICATION_CLICKED_SUFFIX XSTR("GrowlClicked!") 308 | 309 | /*! @defined GROWL_DISTRIBUTED_NOTIFICATION_TIMED_OUT_SUFFIX 310 | * @abstract Part of the name of the distributed notification sent when a supported notification times out without being clicked. 311 | * @discussion When a Growl notification with a click context times out, Growl posts a distributed notification 312 | * whose name is in the format: 313 | * [NSString stringWithFormat:@"%@-%d-%@", appName, pid, GROWL_DISTRIBUTED_NOTIFICATION_TIMED_OUT_SUFFIX] 314 | * The GrowlApplicationBridge responds to this notification by calling a callback in its delegate. 315 | * NOTE: The user may have actually clicked the 'close' button; this triggers an *immediate* time-out of the notification. 316 | */ 317 | #define GROWL_DISTRIBUTED_NOTIFICATION_TIMED_OUT_SUFFIX XSTR("GrowlTimedOut!") 318 | 319 | /*! @group Other symbols */ 320 | /* Symbols which don't fit into any of the other categories. */ 321 | 322 | /*! @defined GROWL_KEY_CLICKED_CONTEXT 323 | * @abstract Used internally as the key for the clickedContext passed over DNC. 324 | * @discussion This key is used in GROWL_NOTIFICATION_CLICKED, and contains the 325 | * click context that was supplied in the original notification. 326 | */ 327 | #define GROWL_KEY_CLICKED_CONTEXT XSTR("ClickedContext") 328 | /*! @defined GROWL_REG_DICT_EXTENSION 329 | * @abstract The filename extension for registration dictionaries. 330 | * @discussion The GrowlApplicationBridge in Growl.framework registers with 331 | * Growl by creating a file with the extension of .(GROWL_REG_DICT_EXTENSION) 332 | * and opening it in the GrowlHelperApp. This happens whether or not Growl is 333 | * running; if it was stopped, it quits immediately without listening for 334 | * notifications. 335 | */ 336 | #define GROWL_REG_DICT_EXTENSION XSTR("growlRegDict") 337 | 338 | 339 | #define GROWL_POSITION_PREFERENCE_KEY @"GrowlSelectedPosition" 340 | 341 | #endif //ndef _GROWLDEFINES_H 342 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 11C74 7 | CFBundleDevelopmentRegion 8 | English 9 | CFBundleExecutable 10 | Growl 11 | CFBundleIdentifier 12 | com.growl.growlframework 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.3.1 19 | CFBundleSignature 20 | GRRR 21 | CFBundleVersion 22 | 1.3.1 23 | DTCompiler 24 | com.apple.compilers.llvm.clang.1_0 25 | DTPlatformBuild 26 | 4D199 27 | DTPlatformVersion 28 | GM 29 | DTSDKBuild 30 | 11C63 31 | DTSDKName 32 | macosx10.7 33 | DTXcode 34 | 0420 35 | DTXcodeBuild 36 | 4D199 37 | NSPrincipalClass 38 | GrowlApplicationBridge 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Versions/A/Growl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/macosx/Frameworks/Growl.framework/Versions/A/Growl -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Versions/A/Headers/Growl.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifdef __OBJC__ 4 | # include 5 | #endif 6 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Versions/A/Headers/GrowlDefines.h: -------------------------------------------------------------------------------- 1 | // 2 | // GrowlDefines.h 3 | // 4 | 5 | #ifndef _GROWLDEFINES_H 6 | #define _GROWLDEFINES_H 7 | 8 | #ifdef __OBJC__ 9 | #define XSTR(x) (@x) 10 | #else 11 | #define XSTR CFSTR 12 | #endif 13 | 14 | /*! @header GrowlDefines.h 15 | * @abstract Defines all the notification keys. 16 | * @discussion Defines all the keys used for registration with Growl and for 17 | * Growl notifications. 18 | * 19 | * Most applications should use the functions or methods of Growl.framework 20 | * instead of posting notifications such as those described here. 21 | * @updated 2004-01-25 22 | */ 23 | 24 | // UserInfo Keys for Registration 25 | #pragma mark UserInfo Keys for Registration 26 | 27 | /*! @group Registration userInfo keys */ 28 | /* @abstract Keys for the userInfo dictionary of a GROWL_APP_REGISTRATION distributed notification. 29 | * @discussion The values of these keys describe the application and the 30 | * notifications it may post. 31 | * 32 | * Your application must register with Growl before it can post Growl 33 | * notifications (and have them not be ignored). However, as of Growl 0.6, 34 | * posting GROWL_APP_REGISTRATION notifications directly is no longer the 35 | * preferred way to register your application. Your application should instead 36 | * use Growl.framework's delegate system. 37 | * See +[GrowlApplicationBridge setGrowlDelegate:] or Growl_SetDelegate for 38 | * more information. 39 | */ 40 | 41 | /*! @defined GROWL_APP_NAME 42 | * @abstract The name of your application. 43 | * @discussion The name of your application. This should remain stable between 44 | * different versions and incarnations of your application. 45 | * For example, "SurfWriter" is a good app name, whereas "SurfWriter 2.0" and 46 | * "SurfWriter Lite" are not. 47 | */ 48 | #define GROWL_APP_NAME XSTR("ApplicationName") 49 | /*! @defined GROWL_APP_ID 50 | * @abstract The bundle identifier of your application. 51 | * @discussion The bundle identifier of your application. This key should 52 | * be unique for your application while there may be several applications 53 | * with the same GROWL_APP_NAME. 54 | * This key is optional. 55 | */ 56 | #define GROWL_APP_ID XSTR("ApplicationId") 57 | /*! @defined GROWL_APP_ICON_DATA 58 | * @abstract The image data for your application's icon. 59 | * @discussion Image data representing your application's icon. This may be 60 | * superimposed on a notification icon as a badge, used as the notification 61 | * icon when a notification-specific icon is not supplied, or ignored 62 | * altogether, depending on the display. Must be in a format supported by 63 | * NSImage, such as TIFF, PNG, GIF, JPEG, BMP, PICT, or PDF. 64 | * 65 | * Optional. Not supported by all display plugins. 66 | */ 67 | #define GROWL_APP_ICON_DATA XSTR("ApplicationIcon") 68 | /*! @defined GROWL_NOTIFICATIONS_DEFAULT 69 | * @abstract The array of notifications to turn on by default. 70 | * @discussion These are the names of the notifications that should be enabled 71 | * by default when your application registers for the first time. If your 72 | * application reregisters, Growl will look here for any new notification 73 | * names found in GROWL_NOTIFICATIONS_ALL, but ignore any others. 74 | */ 75 | #define GROWL_NOTIFICATIONS_DEFAULT XSTR("DefaultNotifications") 76 | /*! @defined GROWL_NOTIFICATIONS_ALL 77 | * @abstract The array of all notifications your application can send. 78 | * @discussion These are the names of all of the notifications that your 79 | * application may post. See GROWL_NOTIFICATION_NAME for a discussion of good 80 | * notification names. 81 | */ 82 | #define GROWL_NOTIFICATIONS_ALL XSTR("AllNotifications") 83 | /*! @defined GROWL_NOTIFICATIONS_HUMAN_READABLE_DESCRIPTIONS 84 | * @abstract A dictionary of human-readable names for your notifications. 85 | * @discussion By default, the Growl UI will display notifications by the names given in GROWL_NOTIFICATIONS_ALL 86 | * which correspond to the GROWL_NOTIFICATION_NAME. This dictionary specifies the human-readable name to display. 87 | * The keys of the dictionary are GROWL_NOTIFICATION_NAME strings; the objects are the human-readable versions. 88 | * For any GROWL_NOTIFICATION_NAME not specific in this dictionary, the GROWL_NOTIFICATION_NAME will be displayed. 89 | * 90 | * This key is optional. 91 | */ 92 | #define GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES XSTR("HumanReadableNames") 93 | /*! @defined GROWL_NOTIFICATIONS_DESCRIPTIONS 94 | * @abstract A dictionary of descriptions of _when_ each notification occurs 95 | * @discussion This is an NSDictionary whose keys are GROWL_NOTIFICATION_NAME strings and whose objects are 96 | * descriptions of _when_ each notification occurs, such as "You received a new mail message" or 97 | * "A file finished downloading". 98 | * 99 | * This key is optional. 100 | */ 101 | #define GROWL_NOTIFICATIONS_DESCRIPTIONS XSTR("NotificationDescriptions") 102 | 103 | /*! @defined GROWL_TICKET_VERSION 104 | * @abstract The version of your registration ticket. 105 | * @discussion Include this key in a ticket plist file that you put in your 106 | * application bundle for auto-discovery. The current ticket version is 1. 107 | */ 108 | #define GROWL_TICKET_VERSION XSTR("TicketVersion") 109 | // UserInfo Keys for Notifications 110 | #pragma mark UserInfo Keys for Notifications 111 | 112 | /*! @group Notification userInfo keys */ 113 | /* @abstract Keys for the userInfo dictionary of a GROWL_NOTIFICATION distributed notification. 114 | * @discussion The values of these keys describe the content of a Growl 115 | * notification. 116 | * 117 | * Not all of these keys are supported by all displays. Only the name, title, 118 | * and description of a notification are universal. Most of the built-in 119 | * displays do support all of these keys, and most other visual displays 120 | * probably will also. But, as of 0.6, the Log, MailMe, and Speech displays 121 | * support only textual data. 122 | */ 123 | 124 | /*! @defined GROWL_NOTIFICATION_NAME 125 | * @abstract The name of the notification. 126 | * @discussion The name of the notification. Note that if you do not define 127 | * GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES when registering your ticket originally this name 128 | * will the one displayed within the Growl preference pane and should be human-readable. 129 | */ 130 | #define GROWL_NOTIFICATION_NAME XSTR("NotificationName") 131 | /*! @defined GROWL_NOTIFICATION_TITLE 132 | * @abstract The title to display in the notification. 133 | * @discussion The title of the notification. Should be very brief. 134 | * The title usually says what happened, e.g. "Download complete". 135 | */ 136 | #define GROWL_NOTIFICATION_TITLE XSTR("NotificationTitle") 137 | /*! @defined GROWL_NOTIFICATION_DESCRIPTION 138 | * @abstract The description to display in the notification. 139 | * @discussion The description should be longer and more verbose than the title. 140 | * The description usually tells the subject of the action, 141 | * e.g. "Growl-0.6.dmg downloaded in 5.02 minutes". 142 | */ 143 | #define GROWL_NOTIFICATION_DESCRIPTION XSTR("NotificationDescription") 144 | /*! @defined GROWL_NOTIFICATION_ICON 145 | * @discussion Image data for the notification icon. Image data must be in a format 146 | * supported by NSImage, such as TIFF, PNG, GIF, JPEG, BMP, PICT, or PDF. 147 | * 148 | * Optional. Not supported by all display plugins. 149 | */ 150 | #define GROWL_NOTIFICATION_ICON_DATA XSTR("NotificationIcon") 151 | /*! @defined GROWL_NOTIFICATION_APP_ICON 152 | * @discussion Image data for the application icon, in case GROWL_APP_ICON does 153 | * not apply for some reason. Image data be in a format supported by NSImage, such 154 | * as TIFF, PNG, GIF, JPEG, BMP, PICT, or PDF. 155 | * 156 | * Optional. Not supported by all display plugins. 157 | */ 158 | #define GROWL_NOTIFICATION_APP_ICON_DATA XSTR("NotificationAppIcon") 159 | /*! @defined GROWL_NOTIFICATION_PRIORITY 160 | * @discussion The priority of the notification as an integer number from 161 | * -2 to +2 (+2 being highest). 162 | * 163 | * Optional. Not supported by all display plugins. 164 | */ 165 | #define GROWL_NOTIFICATION_PRIORITY XSTR("NotificationPriority") 166 | /*! @defined GROWL_NOTIFICATION_STICKY 167 | * @discussion A Boolean number controlling whether the notification is sticky. 168 | * 169 | * Optional. Not supported by all display plugins. 170 | */ 171 | #define GROWL_NOTIFICATION_STICKY XSTR("NotificationSticky") 172 | /*! @defined GROWL_NOTIFICATION_CLICK_CONTEXT 173 | * @abstract Identifies which notification was clicked. 174 | * @discussion An identifier for the notification for clicking purposes. 175 | * 176 | * This will be passed back to the application when the notification is 177 | * clicked. It must be plist-encodable (a data, dictionary, array, number, or 178 | * string object), and it should be unique for each notification you post. 179 | * A good click context would be a UUID string returned by NSProcessInfo or 180 | * CFUUID. 181 | * 182 | * Optional. Not supported by all display plugins. 183 | */ 184 | #define GROWL_NOTIFICATION_CLICK_CONTEXT XSTR("NotificationClickContext") 185 | 186 | /*! @defined GROWL_NOTIFICATION_IDENTIFIER 187 | * @abstract An identifier for the notification for coalescing purposes. 188 | * Notifications with the same identifier fall into the same class; only 189 | * the last notification of a class is displayed on the screen. If a 190 | * notification of the same class is currently being displayed, it is 191 | * replaced by this notification. 192 | * 193 | * Optional. Not supported by all display plugins. 194 | */ 195 | #define GROWL_NOTIFICATION_IDENTIFIER XSTR("GrowlNotificationIdentifier") 196 | 197 | /*! @defined GROWL_APP_PID 198 | * @abstract The process identifier of the process which sends this 199 | * notification. If this field is set, the application will only receive 200 | * clicked and timed out notifications which originate from this process. 201 | * 202 | * Optional. 203 | */ 204 | #define GROWL_APP_PID XSTR("ApplicationPID") 205 | 206 | /*! @defined GROWL_NOTIFICATION_PROGRESS 207 | * @abstract If this key is set, it should contain a double value wrapped 208 | * in a NSNumber which describes some sort of progress (from 0.0 to 100.0). 209 | * If this is key is not set, no progress bar is shown. 210 | * 211 | * Optional. Not supported by all display plugins. 212 | */ 213 | #define GROWL_NOTIFICATION_PROGRESS XSTR("NotificationProgress") 214 | 215 | // Notifications 216 | #pragma mark Notifications 217 | 218 | /*! @group Notification names */ 219 | /* @abstract Names of distributed notifications used by Growl. 220 | * @discussion These are notifications used by applications (directly or 221 | * indirectly) to interact with Growl, and by Growl for interaction between 222 | * its components. 223 | * 224 | * Most of these should no longer be used in Growl 0.6 and later, in favor of 225 | * Growl.framework's GrowlApplicationBridge APIs. 226 | */ 227 | 228 | /*! @defined GROWL_APP_REGISTRATION 229 | * @abstract The distributed notification for registering your application. 230 | * @discussion This is the name of the distributed notification that can be 231 | * used to register applications with Growl. 232 | * 233 | * The userInfo dictionary for this notification can contain these keys: 234 | *
    235 | *
  • GROWL_APP_NAME
  • 236 | *
  • GROWL_APP_ICON_DATA
  • 237 | *
  • GROWL_NOTIFICATIONS_ALL
  • 238 | *
  • GROWL_NOTIFICATIONS_DEFAULT
  • 239 | *
240 | * 241 | * No longer recommended as of Growl 0.6. An alternate method of registering 242 | * is to use Growl.framework's delegate system. 243 | * See +[GrowlApplicationBridge setGrowlDelegate:] or Growl_SetDelegate for 244 | * more information. 245 | */ 246 | #define GROWL_APP_REGISTRATION XSTR("GrowlApplicationRegistrationNotification") 247 | /*! @defined GROWL_APP_REGISTRATION_CONF 248 | * @abstract The distributed notification for confirming registration. 249 | * @discussion The name of the distributed notification sent to confirm the 250 | * registration. Used by the Growl preference pane. Your application probably 251 | * does not need to use this notification. 252 | */ 253 | #define GROWL_APP_REGISTRATION_CONF XSTR("GrowlApplicationRegistrationConfirmationNotification") 254 | /*! @defined GROWL_NOTIFICATION 255 | * @abstract The distributed notification for Growl notifications. 256 | * @discussion This is what it all comes down to. This is the name of the 257 | * distributed notification that your application posts to actually send a 258 | * Growl notification. 259 | * 260 | * The userInfo dictionary for this notification can contain these keys: 261 | *
    262 | *
  • GROWL_NOTIFICATION_NAME (required)
  • 263 | *
  • GROWL_NOTIFICATION_TITLE (required)
  • 264 | *
  • GROWL_NOTIFICATION_DESCRIPTION (required)
  • 265 | *
  • GROWL_NOTIFICATION_ICON
  • 266 | *
  • GROWL_NOTIFICATION_APP_ICON
  • 267 | *
  • GROWL_NOTIFICATION_PRIORITY
  • 268 | *
  • GROWL_NOTIFICATION_STICKY
  • 269 | *
  • GROWL_NOTIFICATION_CLICK_CONTEXT
  • 270 | *
  • GROWL_APP_NAME (required)
  • 271 | *
272 | * 273 | * No longer recommended as of Growl 0.6. Three alternate methods of posting 274 | * notifications are +[GrowlApplicationBridge notifyWithTitle:description:notificationName:iconData:priority:isSticky:clickContext:], 275 | * Growl_NotifyWithTitleDescriptionNameIconPriorityStickyClickContext, and 276 | * Growl_PostNotification. 277 | */ 278 | #define GROWL_NOTIFICATION XSTR("GrowlNotification") 279 | /*! @defined GROWL_PING 280 | * @abstract A distributed notification to check whether Growl is running. 281 | * @discussion This is used by the Growl preference pane. If it receives a 282 | * GROWL_PONG, the preference pane takes this to mean that Growl is running. 283 | */ 284 | #define GROWL_PING XSTR("Honey, Mind Taking Out The Trash") 285 | /*! @defined GROWL_PONG 286 | * @abstract The distributed notification sent in reply to GROWL_PING. 287 | * @discussion GrowlHelperApp posts this in reply to GROWL_PING. 288 | */ 289 | #define GROWL_PONG XSTR("What Do You Want From Me, Woman") 290 | /*! @defined GROWL_IS_READY 291 | * @abstract The distributed notification sent when Growl starts up. 292 | * @discussion GrowlHelperApp posts this when it has begin listening on all of 293 | * its sources for new notifications. GrowlApplicationBridge (in 294 | * Growl.framework), upon receiving this notification, reregisters using the 295 | * registration dictionary supplied by its delegate. 296 | */ 297 | #define GROWL_IS_READY XSTR("Lend Me Some Sugar; I Am Your Neighbor!") 298 | 299 | 300 | /*! @defined GROWL_DISTRIBUTED_NOTIFICATION_CLICKED_SUFFIX 301 | * @abstract Part of the name of the distributed notification sent when a supported notification is clicked. 302 | * @discussion When a Growl notification with a click context is clicked on by 303 | * the user, Growl posts a distributed notification whose name is in the format: 304 | * [NSString stringWithFormat:@"%@-%d-%@", appName, pid, GROWL_DISTRIBUTED_NOTIFICATION_CLICKED_SUFFIX] 305 | * The GrowlApplicationBridge responds to this notification by calling a callback in its delegate. 306 | */ 307 | #define GROWL_DISTRIBUTED_NOTIFICATION_CLICKED_SUFFIX XSTR("GrowlClicked!") 308 | 309 | /*! @defined GROWL_DISTRIBUTED_NOTIFICATION_TIMED_OUT_SUFFIX 310 | * @abstract Part of the name of the distributed notification sent when a supported notification times out without being clicked. 311 | * @discussion When a Growl notification with a click context times out, Growl posts a distributed notification 312 | * whose name is in the format: 313 | * [NSString stringWithFormat:@"%@-%d-%@", appName, pid, GROWL_DISTRIBUTED_NOTIFICATION_TIMED_OUT_SUFFIX] 314 | * The GrowlApplicationBridge responds to this notification by calling a callback in its delegate. 315 | * NOTE: The user may have actually clicked the 'close' button; this triggers an *immediate* time-out of the notification. 316 | */ 317 | #define GROWL_DISTRIBUTED_NOTIFICATION_TIMED_OUT_SUFFIX XSTR("GrowlTimedOut!") 318 | 319 | /*! @group Other symbols */ 320 | /* Symbols which don't fit into any of the other categories. */ 321 | 322 | /*! @defined GROWL_KEY_CLICKED_CONTEXT 323 | * @abstract Used internally as the key for the clickedContext passed over DNC. 324 | * @discussion This key is used in GROWL_NOTIFICATION_CLICKED, and contains the 325 | * click context that was supplied in the original notification. 326 | */ 327 | #define GROWL_KEY_CLICKED_CONTEXT XSTR("ClickedContext") 328 | /*! @defined GROWL_REG_DICT_EXTENSION 329 | * @abstract The filename extension for registration dictionaries. 330 | * @discussion The GrowlApplicationBridge in Growl.framework registers with 331 | * Growl by creating a file with the extension of .(GROWL_REG_DICT_EXTENSION) 332 | * and opening it in the GrowlHelperApp. This happens whether or not Growl is 333 | * running; if it was stopped, it quits immediately without listening for 334 | * notifications. 335 | */ 336 | #define GROWL_REG_DICT_EXTENSION XSTR("growlRegDict") 337 | 338 | 339 | #define GROWL_POSITION_PREFERENCE_KEY @"GrowlSelectedPosition" 340 | 341 | #endif //ndef _GROWLDEFINES_H 342 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Versions/A/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 11C74 7 | CFBundleDevelopmentRegion 8 | English 9 | CFBundleExecutable 10 | Growl 11 | CFBundleIdentifier 12 | com.growl.growlframework 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.3.1 19 | CFBundleSignature 20 | GRRR 21 | CFBundleVersion 22 | 1.3.1 23 | DTCompiler 24 | com.apple.compilers.llvm.clang.1_0 25 | DTPlatformBuild 26 | 4D199 27 | DTPlatformVersion 28 | GM 29 | DTSDKBuild 30 | 11C63 31 | DTSDKName 32 | macosx10.7 33 | DTXcode 34 | 0420 35 | DTXcodeBuild 36 | 4D199 37 | NSPrincipalClass 38 | GrowlApplicationBridge 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Versions/A/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Resources/Info.plist 8 | 9 | SwzGt9RQsuVafBBrfBalB75dCwU= 10 | 11 | 12 | rules 13 | 14 | ^Resources/ 15 | 16 | ^Resources/.*\.lproj/ 17 | 18 | optional 19 | 20 | weight 21 | 1000 22 | 23 | ^Resources/.*\.lproj/locversion.plist$ 24 | 25 | omit 26 | 27 | weight 28 | 1100 29 | 30 | ^version.plist$ 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Versions/Current/Growl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/macosx/Frameworks/Growl.framework/Versions/Current/Growl -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Versions/Current/Headers/Growl.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifdef __OBJC__ 4 | # include 5 | #endif 6 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Versions/Current/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 11C74 7 | CFBundleDevelopmentRegion 8 | English 9 | CFBundleExecutable 10 | Growl 11 | CFBundleIdentifier 12 | com.growl.growlframework 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.3.1 19 | CFBundleSignature 20 | GRRR 21 | CFBundleVersion 22 | 1.3.1 23 | DTCompiler 24 | com.apple.compilers.llvm.clang.1_0 25 | DTPlatformBuild 26 | 4D199 27 | DTPlatformVersion 28 | GM 29 | DTSDKBuild 30 | 11C63 31 | DTSDKName 32 | macosx10.7 33 | DTXcode 34 | 0420 35 | DTXcodeBuild 36 | 4D199 37 | NSPrincipalClass 38 | GrowlApplicationBridge 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Frameworks/Growl.framework/Versions/Current/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Resources/Info.plist 8 | 9 | SwzGt9RQsuVafBBrfBalB75dCwU= 10 | 11 | 12 | rules 13 | 14 | ^Resources/ 15 | 16 | ^Resources/.*\.lproj/ 17 | 18 | optional 19 | 20 | weight 21 | 1000 22 | 23 | ^Resources/.*\.lproj/locversion.plist$ 24 | 25 | omit 26 | 27 | weight 28 | 1100 29 | 30 | ^version.plist$ 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 12F45 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | trsst 11 | CFBundleIconFile 12 | trsst-client-0.2-SNAPSHOT.icns 13 | CFBundleIdentifier 14 | com.trsst.command 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | Trsst 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | 1 27 | DTCompiler 28 | com.apple.compilers.llvm.clang.1_0 29 | DTPlatformBuild 30 | 5B1008 31 | DTPlatformVersion 32 | GM 33 | DTSDKBuild 34 | 13C64 35 | DTSDKName 36 | macosx10.9 37 | DTXcode 38 | 0511 39 | DTXcodeBuild 40 | 5B1008 41 | LSMinimumSystemVersion 42 | 10.8 43 | NSMainNibFile 44 | MainMenu 45 | NSPrincipalClass 46 | NSApplication 47 | JVMRuntime 48 | jdk1.8.0.jdk 49 | JVMMainClassName 50 | com.trsst.Command 51 | JVMArguments 52 | 53 | serve 54 | --api 55 | 56 | JVMAppClasspath 57 | lib/abdera-parser-1.1.3.jar lib/axiom-api-1.2.14.jar lib/geronimo-javamail_1.4_spec-1.7.1.jar lib/apache-mime4j-core-0.7.2.jar lib/axiom-impl-1.2.14.jar lib/geronimo-stax-api_1.0_spec-1.0.1.jar lib/wstx-asl-3.2.6.jar lib/jaxen-1.1.1.jar lib/xml-apis-1.3.02.jar lib/commons-logging-1.0.4.jar lib/abdera-core-1.1.3.jar lib/abdera-i18n-1.1.3.jar lib/geronimo-activation_1.1_spec-1.1.jar lib/commons-codec-1.4.jar lib/abdera-client-1.1.3.jar lib/commons-httpclient-3.1.jar lib/abdera-server-1.1.3.jar lib/servlet-api-2.5.jar lib/mail-1.4.jar lib/activation-1.1.jar lib/abdera-security-1.1.3.jar lib/xercesImpl-2.9.1.jar lib/xalan-2.7.0.jar lib/abdera-extensions-json-1.1.3.jar lib/abdera-extensions-main-1.1.3.jar lib/abdera-extensions-html-1.1.3.jar lib/htmlparser-1.0.5.jar lib/abdera-extensions-rss-1.1.3.jar lib/lucene-core-4.6.0.jar lib/lucene-analyzers-common-4.6.0.jar lib/lucene-queryparser-4.6.0.jar lib/lucene-queries-4.6.0.jar lib/lucene-sandbox-4.6.0.jar lib/jakarta-regexp-1.4.jar lib/commons-cli-20040117.000000.jar lib/jetty-server-9.1.0.v20131115.jar lib/javax.servlet-api-3.1.0.jar lib/jetty-http-9.1.0.v20131115.jar lib/jetty-util-9.1.0.v20131115.jar lib/jetty-io-9.1.0.v20131115.jar lib/jetty-servlet-9.1.0.v20131115.jar lib/jetty-security-9.1.0.v20131115.jar lib/bcprov-jdk15on-1.50.jar lib/bcpkix-jdk15on-1.49.jar lib/guava-13.0.1.jar lib/protobuf-java-2.5.0.jar lib/xmlsec-2.0.0-beta.jar lib/slf4j-api-1.7.5.jar lib/woodstox-core-asl-4.2.0.jar lib/stax-api-1.0-2.jar lib/stax2-api-3.1.1.jar lib/slf4j-jdk14-1.7.5.jar lib/commons-lang3-3.2.1.jar lib/commons-io-1.3.2.jar lib/commons-fileupload-1.3.jar lib/tika-core-1.4.jar lib/jtidy-r938.jar lib/AppleJavaExtensions-1.4.jar 58 | JVMMainJarName 59 | trsst-client-0.2-SNAPSHOT-jfx.jar 60 | JVMPreferencesID 61 | com/trsst/command 62 | JVMOptions 63 | 64 | -Xdock:name=Trsst 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/MacOS/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/macosx/MacOS/.DS_Store -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/MacOS/MacGap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/macosx/MacOS/MacGap -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Resources/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 2 | \cocoascreenfonts1{\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | \vieww9600\viewh8400\viewkind0 5 | } -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Resources/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/macosx/Resources/en.lproj/InfoPlist.strings -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Resources/en.lproj/MainMenu.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/macosx/Resources/en.lproj/MainMenu.nib -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/Resources/en.lproj/Window.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/macosx/Resources/en.lproj/Window.nib -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/trsst-client-0.2-SNAPSHOT-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/macosx/trsst-client-0.2-SNAPSHOT-background.png -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/trsst-client-0.2-SNAPSHOT-post-image.sh: -------------------------------------------------------------------------------- 1 | cd .. 2 | mkdir ./images/dmg.image/trsst-client-0.2-SNAPSHOT.app/Contents/Frameworks 3 | cp -R /tmp/trsst-resources/Frameworks/* ./images/dmg.image/trsst-client-0.2-SNAPSHOT.app/Contents/Frameworks/ 4 | cp -R /tmp/trsst-resources/MacOS/* ./images/dmg.image/trsst-client-0.2-SNAPSHOT.app/Contents/MacOS/ 5 | cp -R /tmp/trsst-resources/Resources/* ./images/dmg.image/trsst-client-0.2-SNAPSHOT.app/Contents/Resources/ 6 | mv ./images/dmg.image/trsst-client-0.2-SNAPSHOT.app/Contents/MacOS/trsst* ./images/dmg.image/trsst-client-0.2-SNAPSHOT.app/Contents/MacOS/trsstd 7 | mv ./images/dmg.image/trsst-client-0.2-SNAPSHOT.app/Contents/MacOS/MacGap ./images/dmg.image/trsst-client-0.2-SNAPSHOT.app/Contents/MacOS/trsst 8 | chmod +x ./images/dmg.image/trsst-client-0.2-SNAPSHOT.app/Contents/MacOS/trsst 9 | -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/trsst-client-0.2-SNAPSHOT-volume.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/macosx/trsst-client-0.2-SNAPSHOT-volume.icns -------------------------------------------------------------------------------- /src/main/deploy/package/macosx/trsst-client-0.2-SNAPSHOT.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/macosx/trsst-client-0.2-SNAPSHOT.icns -------------------------------------------------------------------------------- /src/main/deploy/package/windows/trsst-client-0.2-SNAPSHOT.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/deploy/package/windows/trsst-client-0.2-SNAPSHOT.ico -------------------------------------------------------------------------------- /src/main/java/com/trsst/Crypto.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 mpowers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.trsst; 17 | 18 | import java.io.UnsupportedEncodingException; 19 | import java.security.GeneralSecurityException; 20 | import java.security.InvalidKeyException; 21 | import java.security.Key; 22 | import java.security.MessageDigest; 23 | import java.security.NoSuchAlgorithmException; 24 | import java.security.PrivateKey; 25 | import java.security.PublicKey; 26 | import java.security.SecureRandom; 27 | import java.security.interfaces.ECPrivateKey; 28 | import java.security.interfaces.ECPublicKey; 29 | import java.text.SimpleDateFormat; 30 | import java.util.Date; 31 | 32 | import javax.crypto.BadPaddingException; 33 | import javax.crypto.Cipher; 34 | import javax.crypto.IllegalBlockSizeException; 35 | 36 | import org.bouncycastle.crypto.BufferedBlockCipher; 37 | import org.bouncycastle.crypto.CipherParameters; 38 | import org.bouncycastle.crypto.InvalidCipherTextException; 39 | import org.bouncycastle.crypto.agreement.ECDHBasicAgreement; 40 | import org.bouncycastle.crypto.digests.SHA1Digest; 41 | import org.bouncycastle.crypto.digests.SHA256Digest; 42 | import org.bouncycastle.crypto.engines.AESEngine; 43 | import org.bouncycastle.crypto.engines.IESEngine; 44 | import org.bouncycastle.crypto.generators.KDF2BytesGenerator; 45 | import org.bouncycastle.crypto.macs.HMac; 46 | import org.bouncycastle.crypto.modes.CBCBlockCipher; 47 | import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; 48 | import org.bouncycastle.crypto.params.KeyParameter; 49 | import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; 50 | import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; 51 | import org.bouncycastle.jcajce.provider.asymmetric.ec.IESCipher; 52 | 53 | /** 54 | * Shared utilities to try to keep the cryptography implementation in one place 55 | * for easier review. 56 | * 57 | * @author mpowers 58 | */ 59 | public class Crypto { 60 | 61 | public static byte[] encryptKeyWithIES(byte[] input, long entryId, 62 | PublicKey publicKey, PrivateKey privateKey) 63 | throws GeneralSecurityException { 64 | try { 65 | // BC appears to be happier with BCECPublicKeys: 66 | // see BC's IESCipher.engineInit's check for ECPublicKey 67 | publicKey = new BCECPublicKey((ECPublicKey) publicKey, null); 68 | 69 | return _cryptIES(input, publicKey, true); 70 | } catch (GeneralSecurityException e) { 71 | log.error("Error while encrypting key", e); 72 | throw e; 73 | } 74 | } 75 | 76 | public static byte[] decryptKeyWithIES(byte[] input, long entryId, 77 | PublicKey publicKey, PrivateKey privateKey) 78 | throws GeneralSecurityException { 79 | try { 80 | // BC appears to be happier with BCECPrivateKeys: 81 | privateKey = new BCECPrivateKey((ECPrivateKey) privateKey, null); 82 | 83 | return _cryptIES(input, privateKey, false); 84 | } catch (GeneralSecurityException e) { 85 | log.error("Error while decrypting key", e); 86 | throw new GeneralSecurityException(e); 87 | } 88 | } 89 | 90 | private static byte[] _cryptIES(byte[] input, Key recipient, 91 | boolean forEncryption) throws InvalidKeyException, 92 | IllegalBlockSizeException, BadPaddingException { 93 | IESCipher cipher = new IESCipher(new IESEngine( 94 | new ECDHBasicAgreement(), new KDF2BytesGenerator( 95 | new SHA1Digest()), new HMac(new SHA256Digest()), 96 | new PaddedBufferedBlockCipher(new CBCBlockCipher( 97 | new AESEngine())))); 98 | 99 | cipher.engineInit(forEncryption ? Cipher.ENCRYPT_MODE 100 | : Cipher.DECRYPT_MODE, recipient, new SecureRandom()); 101 | return cipher.engineDoFinal(input, 0, input.length); 102 | } 103 | 104 | public static byte[] generateAESKey() { 105 | byte[] result = new byte[32]; 106 | new SecureRandom().nextBytes(result); 107 | return result; 108 | } 109 | 110 | public static byte[] encryptAES(byte[] input, byte[] key) 111 | throws InvalidCipherTextException { 112 | return _cryptBytesAES(input, key, true); 113 | } 114 | 115 | public static byte[] decryptAES(byte[] input, byte[] key) 116 | throws InvalidCipherTextException { 117 | return _cryptBytesAES(input, key, false); 118 | } 119 | 120 | // h/t Steve Weis, Michael Rogers, and liberationtech 121 | private static byte[] _cryptBytesAES(byte[] input, byte[] key, 122 | boolean forEncryption) throws InvalidCipherTextException { 123 | assert key.length == 32; // 32 bytes == 256 bits 124 | return process(input, new PaddedBufferedBlockCipher(new CBCBlockCipher( 125 | new AESEngine())), new KeyParameter(key), forEncryption); 126 | // note: using zero IV because we generate a new key for every message 127 | } 128 | 129 | // h/t Adam Paynter http://stackoverflow.com/users/41619/ 130 | private static byte[] process(byte[] input, 131 | BufferedBlockCipher bufferedBlockCipher, 132 | CipherParameters cipherParameters, boolean forEncryption) 133 | throws InvalidCipherTextException { 134 | bufferedBlockCipher.init(forEncryption, cipherParameters); 135 | 136 | int inputOffset = 0; 137 | int inputLength = input.length; 138 | 139 | int maximumOutputLength = bufferedBlockCipher 140 | .getOutputSize(inputLength); 141 | byte[] output = new byte[maximumOutputLength]; 142 | int outputOffset = 0; 143 | int outputLength = 0; 144 | 145 | int bytesProcessed; 146 | 147 | bytesProcessed = bufferedBlockCipher.processBytes(input, inputOffset, 148 | inputLength, output, outputOffset); 149 | outputOffset += bytesProcessed; 150 | outputLength += bytesProcessed; 151 | 152 | bytesProcessed = bufferedBlockCipher.doFinal(output, outputOffset); 153 | outputOffset += bytesProcessed; 154 | outputLength += bytesProcessed; 155 | 156 | if (outputLength == output.length) { 157 | return output; 158 | } else { 159 | byte[] truncatedOutput = new byte[outputLength]; 160 | System.arraycopy(output, 0, truncatedOutput, 0, outputLength); 161 | return truncatedOutput; 162 | } 163 | } 164 | 165 | /** 166 | * Computes hashcash proof-of-work stamp for the given input and 167 | * bitstrength. Servers can choose which bitstrength they accept, but we 168 | * recommend at least 20. The colon ":" is a delimiter in hashcash so we 169 | * replace all occurances in a token with ".". 170 | * 171 | * This machine is calculating stamps at a mean rate of 340ms, 694ms, 172 | * 1989ms, 4098ms, and 6563ms for bits of 19, 20, 21, 22, and 23 173 | * respectively. 174 | * 175 | * @param bitstrength 176 | * number of leading zero bits to find 177 | * @param timestamp 178 | * the timestamp/entry-id of the enclosing entry 179 | * @param token 180 | * a feed-id or mention-id or tag 181 | * @return 182 | */ 183 | public static final String computeStamp(int bitstrength, long timestamp, 184 | String token) { 185 | try { 186 | if (token.indexOf(':') != -1) { 187 | token = token.replace(":", "."); 188 | } 189 | String formattedDate = new SimpleDateFormat("YYMMdd") 190 | .format(new Date(timestamp)); 191 | String prefix = "1:" + Integer.toString(bitstrength) + ":" 192 | + formattedDate + ":" + token + "::" 193 | + Long.toHexString(timestamp) + ":"; 194 | int masklength = bitstrength / 8; 195 | byte[] prefixBytes = prefix.getBytes("UTF-8"); 196 | MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); 197 | 198 | int i; 199 | int b; 200 | byte[] hash; 201 | long counter = 0; 202 | while (true) { 203 | sha1.update(prefixBytes); 204 | sha1.update(Long.toHexString(counter).getBytes()); 205 | hash = sha1.digest(); // 20 bytes long 206 | for (i = 0; i < 20; i++) { 207 | b = (i < masklength) ? 0 : 255 >> (bitstrength % 8); 208 | if (b != (b | hash[i])) { 209 | // no match; keep trying 210 | break; 211 | } 212 | if (i == masklength) { 213 | // we're a match: return the stamp 214 | // System.out.println(Common.toHex(hash)); 215 | return prefix + Long.toHexString(counter); 216 | } 217 | } 218 | counter++; 219 | // keep going forever until we find it 220 | } 221 | } catch (UnsupportedEncodingException e) { 222 | log.error("No string encoding found: ", e); 223 | } catch (NoSuchAlgorithmException e) { 224 | log.error("No hash algorithm found: ", e); 225 | } 226 | log.error("Exiting without stamp: should never happen"); 227 | return null; 228 | } 229 | 230 | /** 231 | * Verifies the specified hashcash proof-of-work stamp for the given 232 | * timestamp and token. 233 | * 234 | * @return true if verified, false if failed or invalid. 235 | */ 236 | public static final boolean verifyStamp(String stamp, long timestamp, 237 | String token) { 238 | String[] fields = stamp.split(":"); 239 | 240 | if (fields.length != 7) { 241 | log.info("verifyStamp: invalid number of fields: " + fields.length); 242 | return false; 243 | } 244 | 245 | if (!"1".equals(fields[0])) { 246 | log.info("verifyStamp: invalid version: " + fields[0]); 247 | return false; 248 | } 249 | 250 | int bitstrength; 251 | try { 252 | bitstrength = Integer.parseInt(fields[1]); 253 | } catch (NumberFormatException e) { 254 | log.info("verifyStamp: invalid bit strength: " + fields[1]); 255 | return false; 256 | } 257 | 258 | String formattedDate = new SimpleDateFormat("YYMMdd").format(new Date( 259 | timestamp)); 260 | if (!formattedDate.equals(fields[2])) { 261 | log.info("verifyStamp: invalid date: " + fields[2]); 262 | return false; 263 | } 264 | 265 | if (!token.equals(fields[3])) { 266 | log.info("verifyStamp: invalid token: " + fields[3]); 267 | return false; 268 | } 269 | 270 | // other fields are ignored; 271 | // now verify hash: 272 | try { 273 | int b; 274 | byte[] hash = MessageDigest.getInstance("SHA-1").digest( 275 | stamp.getBytes("UTF-8")); 276 | for (int i = 0; i < 20; i++) { 277 | b = (i < bitstrength / 8) ? 0 : 255 >> (bitstrength % 8); 278 | if (b != (b | hash[i])) { 279 | return false; 280 | } 281 | if (i == bitstrength / 8) { 282 | // stamp is verified 283 | return true; 284 | } 285 | } 286 | } catch (UnsupportedEncodingException e) { 287 | log.error("No string encoding found: ", e); 288 | } catch (NoSuchAlgorithmException e) { 289 | log.error("No hash algorithm found: ", e); 290 | } 291 | 292 | return false; 293 | } 294 | 295 | private final static org.slf4j.Logger log = org.slf4j.LoggerFactory 296 | .getLogger(Crypto.class); 297 | } 298 | -------------------------------------------------------------------------------- /src/main/java/com/trsst/client/AnonymSSLSocketFactory.java: -------------------------------------------------------------------------------- 1 | /* ********************************************************************** ** 2 | ** Copyright notice ** 3 | ** ** 4 | ** (c) 2005-2009 RSSOwl Development Team ** 5 | ** http://www.rssowl.org/ ** 6 | ** ** 7 | ** All rights reserved ** 8 | ** ** 9 | ** This program and the accompanying materials are made available under ** 10 | ** the terms of the Eclipse Public License v1.0 which accompanies this ** 11 | ** distribution, and is available at: ** 12 | ** http://www.rssowl.org/legal/epl-v10.html ** 13 | ** ** 14 | ** A copy is found in the file epl-v10.html and important notices to the ** 15 | ** license from the team is found in the textfile LICENSE.txt distributed ** 16 | ** in this package. ** 17 | ** ** 18 | ** This copyright notice MUST APPEAR in all copies of the file! ** 19 | ** ** 20 | ** Contributors: ** 21 | ** RSSOwl Development Team - initial API and implementation ** 22 | ** ** 23 | ** ********************************************************************** */ 24 | 25 | package com.trsst.client; 26 | 27 | import org.apache.commons.httpclient.ConnectTimeoutException; 28 | import org.apache.commons.httpclient.HttpClientError; 29 | import org.apache.commons.httpclient.params.HttpConnectionParams; 30 | import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory; 31 | 32 | import java.io.IOException; 33 | import java.net.InetAddress; 34 | import java.net.InetSocketAddress; 35 | import java.net.Socket; 36 | import java.net.SocketAddress; 37 | import java.net.UnknownHostException; 38 | import java.security.cert.X509Certificate; 39 | 40 | import javax.net.SocketFactory; 41 | import javax.net.ssl.SSLContext; 42 | import javax.net.ssl.TrustManager; 43 | import javax.net.ssl.X509TrustManager; 44 | 45 | /** 46 | * EasySSLProtocolSocketFactory can be used to create SSL {@link Socket}s that 47 | * accept self-signed certificates. 48 | * 49 | * @author Oleg Kalnichevski 50 | */ 51 | public class AnonymSSLSocketFactory implements SecureProtocolSocketFactory { 52 | private SSLContext fSslcontext; 53 | 54 | /** 55 | * Create the SSL Context. 56 | * 57 | * @return The SSLContext 58 | */ 59 | private static SSLContext createEasySSLContext() { 60 | try { 61 | SSLContext context = SSLContext.getInstance("SSL"); //$NON-NLS-1$ 62 | context.init(null, new TrustManager[] { new X509TrustManager() { 63 | public void checkClientTrusted(X509Certificate[] chain, 64 | String authType) { 65 | } 66 | 67 | public void checkServerTrusted(X509Certificate[] chain, 68 | String authType) { 69 | } 70 | 71 | public X509Certificate[] getAcceptedIssuers() { 72 | return new X509Certificate[] {}; 73 | } 74 | } }, null); 75 | return context; 76 | } catch (Exception e) { 77 | throw new HttpClientError(e.toString()); 78 | } 79 | } 80 | 81 | /* 82 | * @see org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory# 83 | * createSocket(java.net.Socket, java.lang.String, int, boolean) 84 | */ 85 | public Socket createSocket(Socket socket, String host, int port, 86 | boolean autoClose) throws IOException, UnknownHostException { 87 | return getSSLContext().getSocketFactory().createSocket(socket, host, 88 | port, autoClose); 89 | } 90 | 91 | /* 92 | * @see ProtocolSocketFactory#createSocket(java.lang.String,int) 93 | */ 94 | public Socket createSocket(String host, int port) throws IOException, 95 | UnknownHostException { 96 | return getSSLContext().getSocketFactory().createSocket(host, port); 97 | } 98 | 99 | /* 100 | * @see 101 | * ProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress 102 | * ,int) 103 | */ 104 | public Socket createSocket(String host, int port, InetAddress clientHost, 105 | int clientPort) throws IOException, UnknownHostException { 106 | return getSSLContext().getSocketFactory().createSocket(host, port, 107 | clientHost, clientPort); 108 | } 109 | 110 | /** 111 | * Attempts to get a new socket connection to the given host within the 112 | * given time limit. 113 | * 114 | * @param host 115 | * the host name/IP 116 | * @param port 117 | * the port on the host 118 | * @param params 119 | * {@link HttpConnectionParams Http connection parameters} 120 | * @return Socket a new socket 121 | * @throws IOException 122 | * if an I/O error occurs while creating the socket 123 | * @throws UnknownHostException 124 | * if the IP address of the host cannot be determined 125 | */ 126 | public Socket createSocket(final String host, final int port, 127 | final InetAddress localAddress, final int localPort, 128 | final HttpConnectionParams params) throws IOException, 129 | UnknownHostException, ConnectTimeoutException { 130 | if (params == null) 131 | throw new IllegalArgumentException("Parameters may not be null"); //$NON-NLS-1$ 132 | 133 | /* Determine Connection Timeout */ 134 | int timeout = params.getConnectionTimeout(); 135 | SocketFactory socketfactory = getSSLContext().getSocketFactory(); 136 | 137 | /* Timeout is unlimited */ 138 | if (timeout == 0) 139 | return socketfactory.createSocket(host, port, localAddress, 140 | localPort); 141 | 142 | /* Timeout is defined */ 143 | Socket socket = socketfactory.createSocket(); 144 | SocketAddress localaddr = new InetSocketAddress(localAddress, localPort); 145 | SocketAddress remoteaddr = new InetSocketAddress(host, port); 146 | socket.bind(localaddr); 147 | socket.connect(remoteaddr, timeout); 148 | return socket; 149 | } 150 | 151 | /* 152 | * @see java.lang.Object#equals(java.lang.Object) 153 | */ 154 | @Override 155 | public boolean equals(Object obj) { 156 | return ((obj != null) && obj.getClass().equals( 157 | AnonymSSLSocketFactory.class)); 158 | } 159 | 160 | /* 161 | * @see java.lang.Object#hashCode() 162 | */ 163 | @Override 164 | public int hashCode() { 165 | return AnonymSSLSocketFactory.class.hashCode(); 166 | } 167 | 168 | /* 169 | * @return The SSLContext 170 | */ 171 | private SSLContext getSSLContext() { 172 | if (fSslcontext == null) 173 | fSslcontext = createEasySSLContext(); 174 | 175 | return fSslcontext; 176 | } 177 | } -------------------------------------------------------------------------------- /src/main/java/com/trsst/client/EntryOptions.java: -------------------------------------------------------------------------------- 1 | package com.trsst.client; 2 | 3 | import java.security.PrivateKey; 4 | import java.util.Date; 5 | import java.util.LinkedList; 6 | import java.util.List; 7 | 8 | /** 9 | * Builder class for updating a feed and/or creating an entry. 10 | * 11 | * @author mpowers 12 | */ 13 | public class EntryOptions { 14 | 15 | String status; 16 | String verb; 17 | Date publish; 18 | String body; 19 | String url; 20 | String[] mentions; 21 | String[] tags; 22 | String[] recipientIds; 23 | PrivateKey[] decryptionKeys; 24 | EntryOptions publicOptions; 25 | private List mimetype = new LinkedList(); 26 | private List content = new LinkedList(); 27 | 28 | /** 29 | * Create empty default post options. By default, no entry is created, and 30 | * no feed settings are updated, although a new feed may be created if it 31 | * does not already exist. 32 | */ 33 | public EntryOptions() { 34 | } 35 | 36 | /** 37 | * Convenience to reset these options to original state for reuse. 38 | */ 39 | public void reset() { 40 | status = null; 41 | verb = null; 42 | publish = null; 43 | body = null; 44 | url = null; 45 | mentions = null; 46 | tags = null; 47 | mimetype = null; 48 | content = null; 49 | recipientIds = null; 50 | decryptionKeys = null; 51 | publicOptions = null; 52 | } 53 | 54 | /** 55 | * @return the status 56 | */ 57 | public String getStatus() { 58 | return status; 59 | } 60 | 61 | /** 62 | * @param status 63 | * A short text string no longer than 250 characters with no 64 | * markup. 65 | */ 66 | public EntryOptions setStatus(String status) { 67 | this.status = status; 68 | return this; 69 | } 70 | 71 | /** 72 | * @return the verb 73 | */ 74 | public String getVerb() { 75 | return verb; 76 | } 77 | 78 | /** 79 | * @param verb 80 | * An activity streams verb; if unspecified, "post" is implicit. 81 | */ 82 | public EntryOptions setVerb(String verb) { 83 | this.verb = verb; 84 | return this; 85 | } 86 | 87 | /** 88 | * @return the publish date 89 | */ 90 | public Date getPublish() { 91 | return publish; 92 | } 93 | 94 | /** 95 | * @param publish 96 | * The date on which this entry is publicly available, which may 97 | * be in the future. 98 | */ 99 | public EntryOptions setPublish(Date publish) { 100 | this.publish = publish; 101 | return this; 102 | } 103 | 104 | /** 105 | * @return the body 106 | */ 107 | public String getBody() { 108 | return body; 109 | } 110 | 111 | /** 112 | * @param body 113 | * An arbitrarily long text string that may be formatted in 114 | * markdown; no HTML is allowed. 115 | */ 116 | public EntryOptions setBody(String body) { 117 | this.body = body; 118 | return this; 119 | } 120 | 121 | /** 122 | * @return the mentions 123 | */ 124 | public String[] getMentions() { 125 | return mentions; 126 | } 127 | 128 | /** 129 | * @param mentions 130 | * Zero or more feed ids, or aliases to feed ids in the form of 131 | * alias@homeserver 132 | */ 133 | public EntryOptions setMentions(String[] mentions) { 134 | this.mentions = mentions; 135 | return this; 136 | } 137 | 138 | /** 139 | * @return the tags 140 | */ 141 | public String[] getTags() { 142 | return tags; 143 | } 144 | 145 | /** 146 | * @param tags 147 | * Zero or more tags (aka hashtags but without the hash); these 148 | * are equivalent to atom categories. 149 | */ 150 | public EntryOptions setTags(String[] tags) { 151 | this.tags = tags; 152 | return this; 153 | } 154 | 155 | /** 156 | * @return the mimetype parallel array 157 | */ 158 | public String[] getMimetypes() { 159 | return mimetype.toArray(new String[0]); 160 | } 161 | 162 | /** 163 | * @return the content parallel array 164 | */ 165 | public byte[][] getContentData() { 166 | return content.toArray(new byte[0][]); 167 | } 168 | 169 | /** 170 | * @return the size of the content parallel arrays 171 | */ 172 | public int getContentCount() { 173 | return content.size(); 174 | } 175 | 176 | /** 177 | * @param content 178 | * Optional binary content to be uploaded and hosted. 179 | * @throws IllegalArgumentException 180 | * if contentUrl is already set. 181 | */ 182 | public EntryOptions addContentData(byte[] content, String mimetype) { 183 | if (this.url != null) { 184 | throw new IllegalArgumentException( 185 | "Cannot have set both url and data"); 186 | } 187 | this.content.add(content); 188 | this.mimetype.add(mimetype); 189 | return this; 190 | } 191 | 192 | /** 193 | * @return the url 194 | */ 195 | public String getContentUrl() { 196 | return url; 197 | } 198 | 199 | /** 200 | * Sets the optional url to share. Note: this will take precedence over any 201 | * content attachments, which will still be referenced via an enclosure 202 | * link. 203 | * 204 | * @param content 205 | * Optional url to share. 206 | */ 207 | public EntryOptions setContentUrl(String url) { 208 | this.url = url; 209 | return this; 210 | } 211 | 212 | /** 213 | * @return the recipientKey 214 | */ 215 | public String[] getRecipientKeys() { 216 | return recipientIds; 217 | } 218 | 219 | /** 220 | * @param publicOptions 221 | * publicly-readable options for the post that contains the 222 | * encrypted entry data 223 | * @param recipientKey 224 | * encrypts this entry using the specified public key so that 225 | * only that key's owner can read it. 226 | */ 227 | public EntryOptions encryptFor(String[] recipientKey, 228 | EntryOptions publicOptions) { 229 | this.publicOptions = publicOptions; 230 | this.recipientIds = recipientKey; 231 | return this; 232 | } 233 | 234 | /** 235 | * @param publicOptions 236 | * publicly-readable options for the post that contains the 237 | * encrypted entry data 238 | * @param decryptionKeys 239 | * decrypts any encrypted entries using each of the specified 240 | * private keys 241 | */ 242 | public EntryOptions decryptWith(PrivateKey[] decryptionKeys) { 243 | this.decryptionKeys = decryptionKeys; 244 | return this; 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /src/main/java/com/trsst/client/FeedOptions.java: -------------------------------------------------------------------------------- 1 | package com.trsst.client; 2 | 3 | /** 4 | * Builder class for updating a feed and/or creating an entry. 5 | * 6 | * @author mpowers 7 | */ 8 | public class FeedOptions { 9 | 10 | String base; 11 | String name; 12 | String email; 13 | String uri; 14 | String title; 15 | String subtitle; 16 | String icon; 17 | String logo; 18 | boolean asIcon; 19 | boolean asLogo; 20 | 21 | /** 22 | * Create empty default post options. By default, no entry is created, and 23 | * no feed settings are updated, although a new feed may be created if it 24 | * does not already exist. 25 | */ 26 | public FeedOptions() { 27 | } 28 | 29 | /** 30 | * Convenience to reset these options to original state for reuse. 31 | */ 32 | public void reset() { 33 | name = null; 34 | email = null; 35 | uri = null; 36 | title = null; 37 | subtitle = null; 38 | icon = null; 39 | logo = null; 40 | asIcon = false; 41 | asLogo = false; 42 | } 43 | 44 | /** 45 | * @return this feed's home base url. 46 | */ 47 | public String getFeedBase() { 48 | return base; 49 | } 50 | 51 | /** 52 | * @param name 53 | * Specify the home base URL where this feed is permanently 54 | * hosted. 55 | */ 56 | public FeedOptions setBase(String base) { 57 | this.base = base; 58 | return this; 59 | } 60 | 61 | /** 62 | * @return the name 63 | */ 64 | public String getAuthorName() { 65 | return name; 66 | } 67 | 68 | /** 69 | * @param name 70 | * Updates the author name associated with the feed. 71 | */ 72 | public FeedOptions setAuthorName(String name) { 73 | this.name = name; 74 | return this; 75 | } 76 | 77 | /** 78 | * @return the uri 79 | */ 80 | public String getAuthorUri() { 81 | return uri; 82 | } 83 | 84 | /** 85 | * @param email 86 | * Updates the author email associated with the feed. 87 | */ 88 | public FeedOptions setAuthorUri(String uri) { 89 | this.uri = uri; 90 | return this; 91 | } 92 | 93 | /** 94 | * @return the email 95 | */ 96 | public String getAuthorEmail() { 97 | return email; 98 | } 99 | 100 | /** 101 | * @param email 102 | * Updates the author email associated with the feed. 103 | */ 104 | public FeedOptions setAuthorEmail(String email) { 105 | this.email = email; 106 | return this; 107 | } 108 | 109 | /** 110 | * @return the title 111 | */ 112 | public String getFeedTitle() { 113 | return title; 114 | } 115 | 116 | /** 117 | * @param title 118 | * Updates the title of the feed, or empty string to remove. 119 | */ 120 | public FeedOptions setTitle(String title) { 121 | this.title = title; 122 | return this; 123 | } 124 | 125 | /** 126 | * @return the subtitle 127 | */ 128 | public String getFeedSubtitle() { 129 | return subtitle; 130 | } 131 | 132 | /** 133 | * @param subtitle 134 | * Updates the subtitle of the feed, or empty string to remove. 135 | */ 136 | public FeedOptions setSubtitle(String subtitle) { 137 | this.subtitle = subtitle; 138 | return this; 139 | } 140 | 141 | /** 142 | * @return the icon 143 | */ 144 | public String getFeedIcon() { 145 | return icon; 146 | } 147 | 148 | /** 149 | * @param asIcon 150 | * Uses the entry attachment as an icon. 151 | */ 152 | public FeedOptions setAsIcon(boolean asIcon) { 153 | this.asIcon = asIcon; 154 | return this; 155 | } 156 | 157 | /** 158 | * @param icon 159 | * Updates the icon of the feed, or empty string to remove; this 160 | * is the equivalent to a user profile pic. 161 | */ 162 | public FeedOptions setIconURL(String icon) { 163 | this.asIcon = false; 164 | this.icon = icon; 165 | return this; 166 | } 167 | 168 | /** 169 | * @return the logo 170 | */ 171 | public String getFeedLogo() { 172 | return logo; 173 | } 174 | 175 | /** 176 | * @param asLogo 177 | * Uses the entry attachment as a logo. 178 | */ 179 | public FeedOptions setAsLogo(boolean asLogo) { 180 | this.asLogo = asLogo; 181 | return this; 182 | } 183 | 184 | /** 185 | * @param logo 186 | * Updates the logo of the feed, or empty string to remove; this 187 | * is the equivalent to a user background image. 188 | */ 189 | public FeedOptions setLogoURL(String logo) { 190 | this.asLogo = false; 191 | this.logo = logo; 192 | return this; 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /src/main/java/com/trsst/client/MultiPartRequestEntity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. The ASF licenses this file to You 4 | * under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. For additional information regarding 15 | * copyright in this work, please see the NOTICE file in the top level 16 | * directory of this distribution. 17 | */ 18 | package com.trsst.client; 19 | 20 | import java.io.DataOutputStream; 21 | import java.io.IOException; 22 | import java.io.OutputStream; 23 | 24 | import org.apache.abdera.model.Base; 25 | import org.apache.abdera.util.MimeTypeHelper; 26 | import org.apache.commons.codec.binary.Base64; 27 | import org.apache.commons.httpclient.methods.RequestEntity; 28 | 29 | /** 30 | * A RequestEntity that handles an Entry or a Feed and supports multiple 31 | * attachments. Per convention, each content id should match an id referenced in 32 | * an entry with a corresponding digest or the server must reject. 33 | * 34 | * @author mpowers 35 | */ 36 | public class MultiPartRequestEntity implements RequestEntity { 37 | 38 | private final Base base; 39 | private final byte[][] content; 40 | private final String[] contentId; 41 | private final String[] contentType; 42 | private long contentLength; 43 | private String boundary; 44 | 45 | public MultiPartRequestEntity(Base base, byte[][] content, 46 | String[] contentId, String[] contentType) { 47 | this.base = base; 48 | this.content = content; 49 | this.contentId = contentId; 50 | this.contentType = contentType; 51 | this.boundary = boundary != null ? boundary : String.valueOf(System 52 | .currentTimeMillis()); 53 | try { 54 | // dummy output stream to count the content length 55 | contentLength = 0; 56 | writeRequest(new OutputStream() { 57 | @Override 58 | public void write(int b) { 59 | contentLength++; 60 | } 61 | 62 | @Override 63 | public void write(byte[] b, int off, int len) { 64 | contentLength += len; 65 | } 66 | }); 67 | } catch (IOException e) { 68 | this.contentLength = -1; 69 | log.error("Unexpected error while determining content length"); 70 | } 71 | log.debug("MultiPartRequestEntity: contentLength: " + contentLength); 72 | } 73 | 74 | public void writeRequest(OutputStream arg0) throws IOException { 75 | DataOutputStream out = new DataOutputStream(arg0); 76 | out.writeBytes("--" + boundary + "\r\n"); 77 | writeEntry(base, out); 78 | out.writeBytes("--" + boundary + "\r\n"); 79 | if (content != null) { 80 | for (int i = 0; i < content.length; i++) { 81 | writeContent(content[i], contentId[i], contentType[i], out); 82 | out.writeBytes("\r\n" + "--" + boundary + "--"); 83 | } 84 | } 85 | out.flush(); 86 | } 87 | 88 | private static void writeEntry(Base base, DataOutputStream out) 89 | throws IOException { 90 | out.writeBytes("content-type: " + MimeTypeHelper.getMimeType(base) 91 | + "\r\n\r\n"); 92 | base.writeTo(out); 93 | } 94 | 95 | private static void writeContent(byte[] content, String contentId, 96 | String contentType, DataOutputStream out) throws IOException { 97 | if (contentType == null) { 98 | throw new NullPointerException("media content type can't be null"); 99 | } 100 | out.writeBytes("content-type: " + contentType + "\r\n"); 101 | out.writeBytes("content-id: \r\n\r\n"); 102 | out.write(new Base64().encode(content)); 103 | } 104 | 105 | public long getContentLength() { 106 | return contentLength; 107 | } 108 | 109 | public String getContentType() { 110 | return "Multipart/Related; boundary=\"" + boundary + "\";type=\"" 111 | + MimeTypeHelper.getMimeType(base) + "\""; 112 | } 113 | 114 | public boolean isRepeatable() { 115 | return true; 116 | } 117 | 118 | private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(this 119 | .getClass()); 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/com/trsst/server/AbstractMultipartAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. The ASF licenses this file to You 4 | * under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. For additional information regarding 15 | * copyright in this work, please see the NOTICE file in the top level 16 | * directory of this distribution. 17 | */ 18 | package com.trsst.server; 19 | 20 | import java.io.ByteArrayInputStream; 21 | import java.io.ByteArrayOutputStream; 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.io.PushbackInputStream; 25 | import java.util.Collection; 26 | import java.util.Enumeration; 27 | import java.util.HashMap; 28 | import java.util.LinkedList; 29 | import java.util.List; 30 | import java.util.Map; 31 | 32 | import javax.mail.Header; 33 | import javax.mail.MessagingException; 34 | import javax.mail.internet.InternetHeaders; 35 | 36 | import org.apache.abdera.model.Document; 37 | import org.apache.abdera.model.Element; 38 | import org.apache.abdera.model.Source; 39 | import org.apache.abdera.parser.ParseException; 40 | import org.apache.abdera.parser.Parser; 41 | import org.apache.abdera.protocol.server.RequestContext; 42 | import org.apache.abdera.protocol.server.impl.AbstractCollectionAdapter; 43 | import org.apache.abdera.protocol.server.multipart.MultipartInputStream; 44 | import org.apache.abdera.protocol.server.multipart.MultipartRelatedCollectionInfo; 45 | import org.apache.abdera.util.Constants; 46 | import org.apache.abdera.util.MimeTypeHelper; 47 | import org.apache.commons.codec.binary.Base64; 48 | 49 | @SuppressWarnings("unchecked") 50 | public abstract class AbstractMultipartAdapter extends 51 | AbstractCollectionAdapter implements MultipartRelatedCollectionInfo { 52 | 53 | private static final String CONTENT_TYPE_HEADER = "content-type"; 54 | private static final String CONTENT_ID_HEADER = "content-id"; 55 | private static final String START_PARAM = "start"; 56 | private static final String TYPE_PARAM = "type"; 57 | private static final String BOUNDARY_PARAM = "boundary"; 58 | 59 | protected Map accepts; 60 | 61 | public String[] getAccepts(RequestContext request) { 62 | Collection acceptKeys = getAlternateAccepts(request).keySet(); 63 | return acceptKeys.toArray(new String[acceptKeys.size()]); 64 | } 65 | 66 | protected List getMultipartRelatedData( 67 | RequestContext request, InputStream requestData) 68 | throws IOException, ParseException, MessagingException { 69 | 70 | // NOTE: because request.getInputStream() has already been read 71 | // we pass in a seperate input stream containing the request data 72 | MultipartInputStream stream = getMultipartStream(request, requestData); 73 | List result = new LinkedList(); 74 | stream.skipBoundary(); 75 | 76 | String start = request.getContentType().getParameter(START_PARAM); 77 | 78 | Document source = null; 79 | Map entryHeaders = new HashMap(); 80 | InputStream data = null; 81 | Map dataHeaders = new HashMap(); 82 | 83 | Map headers = getHeaders(stream); 84 | 85 | // first part is required to be the feed or entry 86 | if (start == null 87 | || start.length() == 0 88 | || (headers.containsKey(CONTENT_ID_HEADER) && start 89 | .equals(headers.get(CONTENT_ID_HEADER))) 90 | || (headers.containsKey(CONTENT_TYPE_HEADER) && MimeTypeHelper 91 | .isAtom(headers.get(CONTENT_TYPE_HEADER)))) { 92 | source = getEntry(stream, request); 93 | entryHeaders.putAll(headers); 94 | } else { 95 | throw new ParseException("First part was not a feed or entry: " 96 | + headers); 97 | // data = getDataInputStream(multipart); 98 | // dataHeaders.putAll(headers); 99 | } 100 | 101 | try { 102 | while (stream.available() > 0) { 103 | stream.skipBoundary(); 104 | headers = getHeaders(stream); 105 | if (start != null 106 | && (headers.containsKey(CONTENT_ID_HEADER) && start 107 | .equals(headers.get(CONTENT_ID_HEADER))) 108 | && (headers.containsKey(CONTENT_TYPE_HEADER) && MimeTypeHelper 109 | .isAtom(headers.get(CONTENT_TYPE_HEADER)))) { 110 | throw new ParseException( 111 | "Should not have found a second feed or entry: " 112 | + headers); 113 | } else { 114 | data = getDataInputStream(stream); 115 | dataHeaders.putAll(headers); 116 | } 117 | checkMultipartContent(source, dataHeaders, request); 118 | result.add(new MultipartRelatedPost(source, data, entryHeaders, 119 | dataHeaders)); 120 | } 121 | } catch (IOException ioe) { 122 | log.error("Unexpected error parsing multipart data", ioe); 123 | } 124 | return result; 125 | } 126 | 127 | private MultipartInputStream getMultipartStream(RequestContext request, 128 | InputStream inputStream) throws IOException, ParseException, 129 | IllegalArgumentException { 130 | String boundary = request.getContentType().getParameter(BOUNDARY_PARAM); 131 | 132 | if (boundary == null) { 133 | throw new IllegalArgumentException( 134 | "multipart/related stream invalid, boundary parameter is missing."); 135 | } 136 | 137 | boundary = "--" + boundary; 138 | 139 | String type = request.getContentType().getParameter(TYPE_PARAM); 140 | if (!(type != null && MimeTypeHelper.isAtom(type))) { 141 | throw new ParseException( 142 | "multipart/related stream invalid, type parameter should be " 143 | + Constants.ATOM_MEDIA_TYPE); 144 | } 145 | 146 | PushbackInputStream pushBackInput = new PushbackInputStream( 147 | inputStream, 2); 148 | pushBackInput.unread("\r\n".getBytes()); 149 | 150 | return new MultipartInputStream(pushBackInput, boundary.getBytes()); 151 | } 152 | 153 | private void checkMultipartContent(Document entry, 154 | Map dataHeaders, RequestContext request) 155 | throws ParseException { 156 | if (entry == null) { 157 | throw new ParseException( 158 | "multipart/related stream invalid, media link entry is missing"); 159 | } 160 | if (!dataHeaders.containsKey(CONTENT_TYPE_HEADER)) { 161 | throw new ParseException( 162 | "multipart/related stream invalid, data content-type is missing"); 163 | } 164 | if (!isContentTypeAccepted(dataHeaders.get(CONTENT_TYPE_HEADER), 165 | request)) { 166 | throw new ParseException( 167 | "multipart/related stream invalid, content-type " 168 | + dataHeaders.get(CONTENT_TYPE_HEADER) 169 | + " not accepted into this multipart file"); 170 | } 171 | } 172 | 173 | private Map getHeaders(MultipartInputStream multipart) 174 | throws IOException, MessagingException { 175 | Map mapHeaders = new HashMap(); 176 | moveToHeaders(multipart); 177 | InternetHeaders headers = new InternetHeaders(multipart); 178 | 179 | Enumeration
allHeaders = headers.getAllHeaders(); 180 | if (allHeaders != null) { 181 | while (allHeaders.hasMoreElements()) { 182 | Header header = allHeaders.nextElement(); 183 | mapHeaders.put(header.getName().toLowerCase(), 184 | header.getValue()); 185 | } 186 | } 187 | 188 | return mapHeaders; 189 | } 190 | 191 | private boolean moveToHeaders(InputStream stream) throws IOException { 192 | boolean dash = false; 193 | boolean cr = false; 194 | int byteReaded; 195 | 196 | while ((byteReaded = stream.read()) != -1) { 197 | switch (byteReaded) { 198 | case '\r': 199 | cr = true; 200 | dash = false; 201 | break; 202 | case '\n': 203 | if (cr == true) 204 | return true; 205 | dash = false; 206 | break; 207 | case '-': 208 | if (dash == true) { // two dashes 209 | stream.close(); 210 | return false; 211 | } 212 | dash = true; 213 | cr = false; 214 | break; 215 | default: 216 | dash = false; 217 | cr = false; 218 | } 219 | } 220 | return false; 221 | } 222 | 223 | private InputStream getDataInputStream(InputStream stream) 224 | throws IOException { 225 | Base64 base64 = new Base64(); 226 | ByteArrayOutputStream bo = new ByteArrayOutputStream(); 227 | 228 | byte[] buffer = new byte[1024]; 229 | int i; 230 | while ((i = stream.read(buffer)) != -1) { 231 | bo.write(buffer, 0, i); 232 | } 233 | return new ByteArrayInputStream(base64.decode(bo.toByteArray())); 234 | } 235 | 236 | private Document getEntry(InputStream stream, 237 | RequestContext request) throws ParseException, IOException { 238 | Parser parser = request.getAbdera().getParser(); 239 | if (parser == null) 240 | throw new IllegalArgumentException( 241 | "No Parser implementation was provided"); 242 | Document document = parser.parse(stream, request.getResolvedUri() 243 | .toString(), parser.getDefaultParserOptions()); 244 | return (Document) document; 245 | } 246 | 247 | private boolean isContentTypeAccepted(String contentType, 248 | RequestContext request) { 249 | if (getAlternateAccepts(request) == null) { 250 | return false; 251 | } 252 | for (Map.Entry accept : getAlternateAccepts(request) 253 | .entrySet()) { 254 | if (accept.getKey().equalsIgnoreCase(contentType) 255 | && accept.getValue() != null 256 | && accept.getValue().equalsIgnoreCase( 257 | Constants.LN_ALTERNATE_MULTIPART_RELATED)) { 258 | return true; 259 | } 260 | } 261 | return false; 262 | } 263 | 264 | protected class MultipartRelatedPost { 265 | private final Document source; 266 | private final InputStream data; 267 | private final Map entryHeaders; 268 | private final Map dataHeaders; 269 | 270 | public MultipartRelatedPost(Document base, InputStream data, 271 | Map entryHeaders, 272 | Map dataHeaders) { 273 | this.source = base; 274 | this.data = data; 275 | this.entryHeaders = entryHeaders; 276 | this.dataHeaders = dataHeaders; 277 | } 278 | 279 | public Document getSource() { 280 | return source; 281 | } 282 | 283 | public InputStream getData() { 284 | return data; 285 | } 286 | 287 | public Map getEntryHeaders() { 288 | return entryHeaders; 289 | } 290 | 291 | public Map getDataHeaders() { 292 | return dataHeaders; 293 | } 294 | } 295 | 296 | private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(this 297 | .getClass()); 298 | 299 | } 300 | -------------------------------------------------------------------------------- /src/main/java/com/trsst/server/CachingStorage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 mpowers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.trsst.server; 17 | 18 | import java.io.FileNotFoundException; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.util.Date; 22 | import java.util.concurrent.ConcurrentMap; 23 | 24 | import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; 25 | 26 | /** 27 | * A simple passthrough that caches read operations. 28 | * 29 | * @author mpowers 30 | */ 31 | public class CachingStorage implements Storage { 32 | 33 | /** 34 | * Persistent storage delegate used to fetch items not in cache and to 35 | * passthrough write operations. 36 | */ 37 | private Storage persistentStorage; 38 | 39 | /** 40 | * Persistent storage delegate used to fetch items not in cache and to 41 | * passthrough write operations. 42 | */ 43 | private ConcurrentMap cache; 44 | 45 | /** 46 | * Manages index and calls to the specified storage delegate to handle 47 | * individual feed, entry, and resource persistence. 48 | * 49 | * @param delegate 50 | * @throws IOException 51 | */ 52 | public CachingStorage(Storage delegate) throws IOException { 53 | persistentStorage = delegate; 54 | cache = new ConcurrentLinkedHashMap.Builder() 55 | .maximumWeightedCapacity(256).build(); 56 | 57 | } 58 | 59 | private static char DELIMITER = 0; 60 | 61 | private static final String tokenize(Object... args) { 62 | StringBuffer buf = new StringBuffer(); 63 | for (Object arg : args) { 64 | buf.append(arg).append(DELIMITER); 65 | } 66 | return buf.toString(); 67 | } 68 | 69 | private static Object NOT_FOUND = "NOT_FOUND"; 70 | 71 | private Object get(String token) { 72 | if (cache.containsKey(token)) { 73 | return cache.get(token); 74 | } 75 | return NOT_FOUND; 76 | } 77 | 78 | private void put(String token, Object value) { 79 | cache.put(token, value); 80 | } 81 | 82 | private void purge(String prefix) { 83 | // purge all keys with prefix 84 | for (String key : cache.keySet()) { 85 | if (key.startsWith(prefix)) { 86 | cache.remove(key); 87 | } 88 | } 89 | } 90 | 91 | public String[] getFeedIds(int start, int length) { 92 | String token = tokenize("getFeedIds", start, length); 93 | Object result = get(token); 94 | if (result == NOT_FOUND) { 95 | result = persistentStorage.getFeedIds(start, length); 96 | put(token, result); 97 | } 98 | return (String[]) result; 99 | } 100 | 101 | public String[] getCategories(int start, int length) { 102 | String token = tokenize("getCategories", start, length); 103 | Object result = get(token); 104 | if (result == NOT_FOUND) { 105 | result = persistentStorage.getCategories(start, length); 106 | put(token, result); 107 | } 108 | return (String[]) result; 109 | } 110 | 111 | public int getEntryCount(Date after, Date before, String query, 112 | String[] mentions, String[] tags, String verb) { 113 | return getEntryCountForFeedId(null, after, before, query, mentions, 114 | tags, verb); 115 | } 116 | 117 | public int getEntryCountForFeedId(String feedId, Date after, Date before, 118 | String search, String[] mentions, String[] tags, String verb) { 119 | String token = tokenize(feedId, "getEntryCountForFeedId", after, 120 | before, search, mentions, tags, verb); 121 | Object result = get(token); 122 | if (result == NOT_FOUND) { 123 | result = persistentStorage.getEntryCountForFeedId(feedId, after, 124 | before, search, mentions, tags, verb); 125 | put(token, result); 126 | } 127 | return ((Integer) result).intValue(); 128 | } 129 | 130 | public String[] getEntryIds(int start, int length, Date after, Date before, 131 | String search, String[] mentions, String[] tags, String verb) { 132 | String token = tokenize("getEntryIds", start, length, after, before, 133 | search, mentions, tags, verb); 134 | Object result = get(token); 135 | if (result == NOT_FOUND) { 136 | result = persistentStorage.getEntryIds(start, length, after, 137 | before, search, mentions, tags, verb); 138 | put(token, result); 139 | } 140 | return (String[]) result; 141 | } 142 | 143 | public long[] getEntryIdsForFeedId(String feedId, int start, int length, 144 | Date after, Date before, String search, String[] mentions, 145 | String[] tags, String verb) { 146 | String token = tokenize(feedId, "getEntryIdsForFeedId", start, length, 147 | after, before, search, mentions, tags, verb); 148 | Object result = get(token); 149 | if (result == NOT_FOUND) { 150 | result = persistentStorage.getEntryIdsForFeedId(feedId, start, 151 | length, after, before, search, mentions, tags, verb); 152 | put(token, result); 153 | } 154 | return (long[]) result; 155 | } 156 | 157 | public String readFeed(String feedId) throws FileNotFoundException, 158 | IOException { 159 | String token = tokenize(feedId, "readFeed"); 160 | Object result = get(token); 161 | if (result == NOT_FOUND) { 162 | result = persistentStorage.readFeed(feedId); 163 | put(token, result); 164 | } 165 | return (String) result; 166 | } 167 | 168 | public void updateFeed(String feedId, Date lastUpdated, String content) 169 | throws IOException { 170 | persistentStorage.updateFeed(feedId, lastUpdated, content); 171 | purge(feedId); 172 | } 173 | 174 | public String readEntry(String feedId, long entryId) 175 | throws FileNotFoundException, IOException { 176 | String token = tokenize(feedId, "readEntry", entryId); 177 | Object result = get(token); 178 | if (result == NOT_FOUND) { 179 | result = persistentStorage.readEntry(feedId, entryId); 180 | put(token, result); 181 | } 182 | return (String) result; 183 | } 184 | 185 | public void updateEntry(String feedId, long entryId, Date publishDate, 186 | String content) throws IOException { 187 | persistentStorage.updateEntry(feedId, entryId, publishDate, content); 188 | purge(feedId); 189 | } 190 | 191 | public void deleteEntry(String feedId, long entryId) 192 | throws FileNotFoundException, IOException { 193 | persistentStorage.deleteEntry(feedId, entryId); 194 | purge(feedId); 195 | } 196 | 197 | public String readFeedEntryResourceType(String feedId, long entryId, 198 | String resourceId) throws FileNotFoundException, IOException { 199 | String token = tokenize(feedId, "readFeedEntryResourceType", entryId, 200 | resourceId); 201 | Object result = get(token); 202 | if (result == NOT_FOUND) { 203 | result = persistentStorage.readFeedEntryResourceType(feedId, 204 | entryId, resourceId); 205 | put(token, result); 206 | } 207 | return (String) result; 208 | } 209 | 210 | public InputStream readFeedEntryResource(String feedId, long entryId, 211 | String resourceId) throws FileNotFoundException, IOException { 212 | // don't cache binary content 213 | return persistentStorage.readFeedEntryResource(feedId, entryId, 214 | resourceId); 215 | } 216 | 217 | public void updateFeedEntryResource(String feedId, long entryId, 218 | String resourceId, String mimeType, Date publishDate, byte[] data) 219 | throws IOException { 220 | persistentStorage.updateFeedEntryResource(feedId, entryId, resourceId, 221 | mimeType, publishDate, data); 222 | } 223 | 224 | public void deleteFeedEntryResource(String feedId, long entryId, 225 | String resourceId) throws IOException { 226 | persistentStorage.deleteFeedEntryResource(feedId, entryId, resourceId); 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /src/main/java/com/trsst/server/FileStorage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 mpowers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.trsst.server; 17 | 18 | import java.io.BufferedInputStream; 19 | import java.io.BufferedOutputStream; 20 | import java.io.File; 21 | import java.io.FileFilter; 22 | import java.io.FileInputStream; 23 | import java.io.FileNotFoundException; 24 | import java.io.FileOutputStream; 25 | import java.io.IOException; 26 | import java.io.InputStream; 27 | import java.io.InputStreamReader; 28 | import java.io.OutputStream; 29 | import java.io.OutputStreamWriter; 30 | import java.io.Reader; 31 | import java.io.Writer; 32 | import java.net.URLConnection; 33 | import java.util.Arrays; 34 | import java.util.Comparator; 35 | import java.util.Date; 36 | import java.util.LinkedList; 37 | import java.util.List; 38 | 39 | import com.trsst.Common; 40 | 41 | /** 42 | * Dumb file persistence for small nodes and test cases. This will get you up 43 | * and running quickly, but you'll want to replace it with an implementation 44 | * backed by some kind of database or document store. 45 | * 46 | * Files are placed in a "trsstd" directory inside of your home directory, or 47 | * inside the directory in the system property "com.trsst.storage" if specified. 48 | * 49 | * @author mpowers 50 | */ 51 | public class FileStorage implements Storage { 52 | 53 | public static final String FEED_XML = "feed.xml"; 54 | public static final String ENTRY_SUFFIX = ".atom"; 55 | public static final String ENCODING = "UTF-8"; 56 | 57 | private File root; 58 | 59 | public FileStorage() { 60 | this(Common.getServerRoot()); 61 | } 62 | 63 | public FileStorage(File root) { 64 | this.root = root; 65 | System.err.println("File storage serving from: " + root); 66 | } 67 | 68 | public String[] getFeedIds(int start, int length) { 69 | /* Returns feeds for which we have a keystore. */ 70 | File[] files = root.listFiles(); 71 | if (files == null) { 72 | return new String[0]; 73 | } 74 | List result = new LinkedList(); 75 | int i; 76 | for (File f : files) { 77 | i = f.getName().indexOf(Common.KEY_EXTENSION); 78 | if (i != -1) { 79 | result.add(Common.unescapeHTML(f.getName().substring(0, i))); 80 | } 81 | } 82 | return result.toArray(new String[0]); 83 | } 84 | 85 | public String[] getCategories(int start, int length) { 86 | 87 | // TODO: implement category trackers 88 | 89 | return new String[0]; 90 | } 91 | 92 | public int getEntryCount(Date after, Date before, String query, 93 | String[] mentions, String[] tags, String verb) { 94 | // not supported 95 | return -1; 96 | } 97 | 98 | public String[] getEntryIds(int start, int length, Date after, Date before, 99 | String query, String[] mentions, String[] tags, String verb) { 100 | // not supported 101 | return null; 102 | } 103 | 104 | public int getEntryCountForFeedId(String feedId, Date after, Date before, 105 | String query, String[] mentions, String[] tags, String verb) { 106 | File[] files = new File(root, feedId).listFiles(new FileFilter() { 107 | public boolean accept(File file) { 108 | return file.getName().toLowerCase().endsWith(ENTRY_SUFFIX); 109 | } 110 | }); 111 | return files.length; 112 | } 113 | 114 | public long[] getEntryIdsForFeedId(String feedId, int start, int length, 115 | Date after, Date before, String query, String[] mentions, 116 | String[] tags, String verb) { 117 | if (start < 0 || length < 1) { 118 | throw new IllegalArgumentException("Invalid range: start: " + start 119 | + " : length: " + length); 120 | } 121 | long[] all = getEntryIdsForFeedId(feedId, after, before); 122 | if (start >= all.length) { 123 | return new long[0]; 124 | } 125 | 126 | // TODO: implement query/tag/mention/verb filter 127 | 128 | int end = Math.min(start + length, all.length); 129 | long[] result = new long[end - start]; 130 | System.arraycopy(all, start, result, 0, result.length); 131 | return result; 132 | } 133 | 134 | public String readFeed(String feedId) throws FileNotFoundException, 135 | IOException { 136 | return readStringFromFile(getFeedFileForFeedId(feedId)); 137 | } 138 | 139 | public void updateFeed(String feedId, Date lastUpdated, String feed) 140 | throws FileNotFoundException, IOException { 141 | File file = getFeedFileForFeedId(feedId); 142 | writeStringToFile(feed, file); 143 | if (lastUpdated != null) { 144 | file.setLastModified(lastUpdated.getTime()); 145 | } 146 | } 147 | 148 | public String readEntry(String feedId, long entryId) 149 | throws FileNotFoundException, IOException { 150 | return readStringFromFile(getEntryFileForFeedEntry(feedId, entryId)); 151 | } 152 | 153 | public void updateEntry(String feedId, long entryId, Date publishDate, 154 | String entry) throws IOException { 155 | File file = getEntryFileForFeedEntry(feedId, entryId); 156 | writeStringToFile(entry, file); 157 | if (publishDate != null) { 158 | file.setLastModified(publishDate.getTime()); 159 | } 160 | } 161 | 162 | public void deleteEntry(String feedId, long entryId) throws IOException { 163 | File file = getEntryFileForFeedEntry(feedId, entryId); 164 | if (file.exists()) { 165 | file.delete(); 166 | } 167 | } 168 | 169 | public String readFeedEntryResourceType(String feedId, long entryId, 170 | String resourceId) throws IOException { 171 | return getMimeTypeForFile(getResourceFileForFeedEntry(feedId, entryId, 172 | resourceId)); 173 | } 174 | 175 | public InputStream readFeedEntryResource(String feedId, long entryId, 176 | String resourceId) throws IOException { 177 | return new BufferedInputStream(new FileInputStream( 178 | getResourceFileForFeedEntry(feedId, entryId, resourceId))); 179 | } 180 | 181 | public void updateFeedEntryResource(String feedId, long entryId, 182 | String resourceId, String mimetype, Date publishDate, byte[] data) 183 | throws IOException { 184 | File file = getResourceFileForFeedEntry(feedId, entryId, resourceId); 185 | OutputStream output = new BufferedOutputStream(new FileOutputStream( 186 | file)); 187 | try { 188 | output.write(data, 0, data.length); 189 | output.flush(); 190 | System.err.println("wrote: " + file.getAbsolutePath()); 191 | } finally { 192 | try { 193 | output.close(); 194 | } catch (IOException ioe) { 195 | // suppress any futher error on closing 196 | } 197 | } 198 | if (publishDate != null) { 199 | file.setLastModified(publishDate.getTime()); 200 | } 201 | } 202 | 203 | public void deleteFeedEntryResource(String feedId, long entryId, 204 | String resourceId) { 205 | File file = getResourceFileForFeedEntry(feedId, entryId, resourceId); 206 | if (file.exists()) { 207 | file.delete(); 208 | } 209 | } 210 | 211 | private static final String getMimeTypeForFile(File file) { 212 | return URLConnection.getFileNameMap().getContentTypeFor(file.getName()); 213 | } 214 | 215 | private long[] getEntryIdsForFeedId(String feedId, Date after, Date before) { 216 | feedId = Common.encodeURL(feedId); 217 | final long afterTime = after != null ? after.getTime() : 0; 218 | final long beforeTime = before != null ? before.getTime() : 0; 219 | File[] files = new File(root, feedId).listFiles(new FileFilter() { 220 | public boolean accept(File file) { 221 | if ((file.getName().toLowerCase().endsWith(ENTRY_SUFFIX)) 222 | && (afterTime == 0 || file.lastModified() > afterTime) 223 | && (beforeTime == 0 || file.lastModified() < beforeTime)) { 224 | return true; 225 | } 226 | return false; 227 | } 228 | }); 229 | Arrays.sort(files, new Comparator() { 230 | public int compare(File o1, File o2) { 231 | // System.out.println( new Date(o1.lastModified() ) + " : " + 232 | // new Date(o2.lastModified()) ); 233 | return o1.lastModified() > o2.lastModified() ? -1 : o1 234 | .lastModified() < o2.lastModified() ? 1 : 0; 235 | } 236 | }); 237 | String name; 238 | int suffix = ENTRY_SUFFIX.length(); 239 | long[] result = new long[files.length]; 240 | for (int i = 0; i < files.length; i++) { 241 | name = files[i].getName(); 242 | result[i] = Long.parseLong( 243 | name.substring(0, name.length() - suffix), 16); 244 | } 245 | return result; 246 | } 247 | 248 | private static final String readStringFromFile(File file) 249 | throws IOException { 250 | StringBuffer result = new StringBuffer(); 251 | Reader reader = null; 252 | try { 253 | reader = new InputStreamReader(new BufferedInputStream( 254 | new FileInputStream(file)), ENCODING); 255 | int c; 256 | char[] buf = new char[256]; 257 | while ((c = reader.read(buf)) > 0) { 258 | result.append(buf, 0, c); 259 | } 260 | } finally { 261 | try { 262 | if (reader != null) { 263 | reader.close(); 264 | } 265 | } catch (IOException ioe) { 266 | // suppress any futher error on closing 267 | } 268 | } 269 | return result.toString(); 270 | } 271 | 272 | private static final void writeStringToFile(String text, File file) 273 | throws IOException { 274 | Writer writer = null; 275 | try { 276 | if (!file.getParentFile().exists()) { 277 | file.getParentFile().mkdirs(); // ensure directory exists 278 | } 279 | writer = new OutputStreamWriter(new BufferedOutputStream( 280 | new FileOutputStream(file)), ENCODING); 281 | writer.write(text); 282 | writer.flush(); 283 | System.err.println("wrote: " + file.getAbsolutePath()); 284 | } finally { 285 | try { 286 | if (writer != null) { 287 | writer.close(); 288 | } 289 | } catch (IOException ioe) { 290 | // suppress any futher error on closing 291 | } 292 | } 293 | } 294 | 295 | public File getFeedFileForFeedId(String feedId) { 296 | feedId = Common.encodeURL(feedId); 297 | return new File(new File(root, feedId), FEED_XML); 298 | } 299 | 300 | public File getEntryFileForFeedEntry(String feedId, long entryId) { 301 | feedId = Common.encodeURL(feedId); 302 | return new File(new File(root, feedId), 303 | Long.toHexString(entryId) + ENTRY_SUFFIX); 304 | } 305 | 306 | public File getResourceFileForFeedEntry(String feedId, long entryId, 307 | String resourceid) { 308 | feedId = Common.encodeURL(feedId); 309 | return new File(new File(root, feedId), 310 | Long.toHexString(entryId) + '-' + resourceid); 311 | } 312 | 313 | } -------------------------------------------------------------------------------- /src/main/java/com/trsst/server/HomeAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 mpowers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.trsst.server; 17 | 18 | import java.io.FileNotFoundException; 19 | import java.io.IOException; 20 | import java.util.Date; 21 | import java.util.Iterator; 22 | import java.util.List; 23 | import java.util.TreeMap; 24 | 25 | import org.apache.abdera.Abdera; 26 | import org.apache.abdera.model.Document; 27 | import org.apache.abdera.model.Entry; 28 | import org.apache.abdera.model.Feed; 29 | import org.apache.abdera.parser.ParseException; 30 | import org.apache.abdera.protocol.server.RequestContext; 31 | import org.apache.abdera.protocol.server.RequestContext.Scope; 32 | import org.apache.abdera.protocol.server.context.RequestContextWrapper; 33 | 34 | import com.trsst.Common; 35 | 36 | /** 37 | * Customized adapter used to serve aggregate feeds containing global search 38 | * results. 39 | * 40 | * @author mpowers 41 | */ 42 | 43 | public class HomeAdapter extends TrsstAdapter { 44 | 45 | public HomeAdapter(String feedId, Storage storage) 46 | throws FileNotFoundException, IOException { 47 | super(feedId, storage); 48 | } 49 | 50 | /** 51 | * Returns the current feed to service this request, fetching from the 52 | * current request, from local storage, or from remote peers as needed. 53 | */ 54 | protected Feed currentFeed(RequestContext request) throws ParseException, 55 | FileNotFoundException, IOException { 56 | Feed feed = null; 57 | RequestContextWrapper wrapper = new RequestContextWrapper(request); 58 | // System.err.println(new Date().toString() + " " 59 | // + wrapper.getTargetPath()); 60 | 61 | // fetch from request context 62 | feed = (Feed) wrapper.getAttribute(Scope.REQUEST, "com.trsst.Feed"); 63 | if (feed != null) { 64 | // shortcut for very common case 65 | return feed; 66 | } 67 | 68 | feed = Abdera.getInstance().newFeed(); 69 | feed.setId(canonicalFeedIdForQuery(request)); 70 | 71 | // build a title string from query params 72 | List values; 73 | String title = ""; 74 | values = request.getParameters("tag"); 75 | if (values != null) { 76 | for (String value : values) { 77 | title = title + '#' + value + ' '; 78 | } 79 | } 80 | values = request.getParameters("mention"); 81 | if (values != null) { 82 | for (String value : values) { 83 | title = title + '@' + value + ' '; 84 | } 85 | } 86 | String query = request.getParameter("q"); 87 | if (query != null) { 88 | title = title + query; 89 | } 90 | title = title.trim(); 91 | if (title.length() == 0) { 92 | title = "Search Results"; 93 | } 94 | feed.setTitle(title); 95 | 96 | // default to one month ago in case of zero results 97 | feed.setUpdated(new Date(System.currentTimeMillis() - 1000 * 60 * 60 98 | * 24 * 30)); 99 | 100 | if (feed != null) { 101 | if (feed.getEntries().size() == 0 102 | || wrapper.getParameter("sync") != null) { 103 | // no local results: check the relays now 104 | pullFromRelay(feedId, request); 105 | } else { 106 | // we have some results: return these and check relays later 107 | pullLaterFromRelay(feedId, request); 108 | } 109 | } 110 | 111 | // store in request context 112 | wrapper.setAttribute(Scope.REQUEST, "com.trsst.Feed", feed); 113 | return feed; 114 | } 115 | 116 | /** 117 | * Eliminates paging parameters and sorts remaining query parameters to 118 | * construct a feed id string of the form "?key=value&key=value" 119 | * 120 | * @param requestment 121 | * @return 122 | */ 123 | public static String canonicalFeedIdForQuery(RequestContext request) { 124 | // collate and sort query parameters 125 | TreeMap> map = new TreeMap>(); 126 | String[] params = request.getParameterNames(); 127 | for (String param : params) { 128 | map.put(param, request.getParameters(param)); 129 | } 130 | 131 | // remove params used for paging 132 | map.remove("page"); 133 | map.remove("count"); 134 | map.remove("before"); 135 | map.remove("after"); 136 | 137 | // reassemble ordered query string 138 | StringBuffer buf = new StringBuffer("urn:feed:?"); 139 | String param; 140 | List values; 141 | Iterator iterator = map.keySet().iterator(); 142 | while (iterator.hasNext()) { 143 | param = iterator.next(); 144 | values = map.get(param); 145 | for (String value : values) { 146 | buf.append(Common.encodeURL(param)); 147 | buf.append('='); 148 | buf.append(Common.encodeURL(value)); 149 | buf.append('&'); 150 | } 151 | } 152 | // remove trailing ampersand 153 | return buf.substring(0, buf.length() - 1); 154 | } 155 | 156 | /** 157 | * Overridden to populate aggregate feed with global query results. 158 | * 159 | * @return the total number of entries matching the query. 160 | */ 161 | @Override 162 | protected int addEntriesFromStorage(Feed feed, int start, int length, 163 | Date after, Date before, String query, String[] mentions, 164 | String[] tags, String verb) { 165 | String[] entryIds = persistence.getEntryIds(0, length, after, before, 166 | query, mentions, tags, verb); 167 | Document entryDoc; 168 | String feedId; 169 | Feed parentFeed; 170 | long entryId; 171 | String urn; 172 | Entry entry; 173 | Date updated = null; 174 | int end = Math.min(entryIds.length, start + length); 175 | for (int i = start; i < end; i++) { 176 | urn = entryIds[i]; 177 | feedId = urn.substring(0, urn.lastIndexOf(':')); 178 | entryId = Common.toEntryId(urn); 179 | parentFeed = fetchFeedFromStorage(feedId, persistence); 180 | entryDoc = getEntry(persistence, feedId, entryId); 181 | if (entryDoc != null) { 182 | entry = (Entry) entryDoc.getRoot().clone(); 183 | if (updated == null || updated.before(entry.getUpdated())) { 184 | updated = entry.getUpdated(); 185 | } 186 | if (parentFeed != null) { 187 | // specify xmlbase if known 188 | entry.setBaseUri(parentFeed.getBaseUri()); 189 | // NOTE: clients should not persist 190 | // xmlbase on aggregate feeds 191 | } 192 | feed.addEntry(entry); 193 | } else { 194 | log.error("Could not find entry for id: " + feedId + " : " 195 | + Long.toHexString(entryId)); 196 | } 197 | } 198 | if (updated != null) { 199 | // set to date of most recent entry 200 | feed.setUpdated(updated); 201 | } 202 | return entryIds.length; 203 | } 204 | 205 | /** 206 | * Counts entries specified search parameters. 207 | * 208 | * @return the total number of entries matching the query. 209 | */ 210 | @Override 211 | protected int countEntriesFromStorage(Date after, Date before, 212 | String query, String[] mentions, String[] tags, String verb) { 213 | return persistence.getEntryCount(after, before, query, mentions, tags, 214 | verb); 215 | } 216 | 217 | private static final org.slf4j.Logger log = org.slf4j.LoggerFactory 218 | .getLogger(HomeAdapter.class); 219 | 220 | } -------------------------------------------------------------------------------- /src/main/java/com/trsst/server/Server.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 mpowers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.trsst.server; 17 | 18 | import java.io.IOException; 19 | import java.math.BigInteger; 20 | import java.net.InetAddress; 21 | import java.net.MalformedURLException; 22 | import java.net.ServerSocket; 23 | import java.net.URL; 24 | import java.net.UnknownHostException; 25 | import java.security.KeyPair; 26 | import java.security.KeyPairGenerator; 27 | import java.security.KeyStore; 28 | import java.security.KeyStoreException; 29 | import java.security.NoSuchAlgorithmException; 30 | import java.security.SecureRandom; 31 | import java.security.cert.Certificate; 32 | import java.security.cert.CertificateException; 33 | import java.util.Date; 34 | 35 | import org.apache.abdera.protocol.server.servlet.AbderaServlet; 36 | import org.bouncycastle.asn1.x500.X500Name; 37 | import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; 38 | import org.bouncycastle.cert.X509v3CertificateBuilder; 39 | import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; 40 | import org.bouncycastle.operator.ContentSigner; 41 | import org.bouncycastle.operator.OperatorCreationException; 42 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 43 | import org.eclipse.jetty.server.Connector; 44 | import org.eclipse.jetty.server.HttpConfiguration; 45 | import org.eclipse.jetty.server.HttpConnectionFactory; 46 | import org.eclipse.jetty.server.SecureRequestCustomizer; 47 | import org.eclipse.jetty.server.ServerConnector; 48 | import org.eclipse.jetty.server.SslConnectionFactory; 49 | import org.eclipse.jetty.servlet.ServletContextHandler; 50 | import org.eclipse.jetty.servlet.ServletHolder; 51 | import org.eclipse.jetty.util.ssl.SslContextFactory; 52 | 53 | /** 54 | * Jetty-specific configuration to host an Abdera servlet that is configured to 55 | * serve the Trsst protocol. 56 | * 57 | * @author mpowers 58 | */ 59 | public class Server { 60 | private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(this 61 | .getClass()); 62 | 63 | int port; 64 | String path; 65 | boolean secure; 66 | private org.eclipse.jetty.server.Server server; 67 | private ServletContextHandler context; 68 | 69 | public Server() throws Exception { 70 | this(0, null); 71 | } 72 | 73 | public Server(int port) throws Exception { 74 | this(port, null); 75 | } 76 | 77 | public Server(int port, String path) throws Exception { 78 | this(port, path, false); 79 | } 80 | 81 | public Server(boolean secure) throws Exception { 82 | this(0, null, secure); 83 | } 84 | 85 | public Server(String path, boolean secure) throws Exception { 86 | this(0, path, secure); 87 | } 88 | 89 | public Server(int port, String path, boolean secure) throws Exception { 90 | try { 91 | if (path != null) { 92 | if (path.endsWith("/")) { 93 | path = path.substring(0, path.length() - 1); 94 | } 95 | if (!path.startsWith("/")) { 96 | path = "/" + path; 97 | } 98 | } else { 99 | path = ""; 100 | } 101 | if (port == 0) { 102 | port = allocatePort(); 103 | } 104 | 105 | this.secure = secure; 106 | this.port = port; 107 | this.path = path; 108 | start(); 109 | } catch (Exception ioe) { 110 | log.error("could not start server on " + port + " : " + path, ioe); 111 | throw ioe; 112 | } 113 | } 114 | 115 | /** 116 | * Generates a new keystore containing a self-signed certificate. Would 117 | * prefer anon SSL ciphers, but this works albeit with scary warnings. 118 | * 119 | * @return a keystore to secure SSL connections. 120 | */ 121 | private KeyStore getKeyStore() { 122 | try { 123 | KeyPairGenerator keyPairGenerator = KeyPairGenerator 124 | .getInstance("RSA"); 125 | keyPairGenerator.initialize(1024); 126 | KeyPair kp = keyPairGenerator.generateKeyPair(); 127 | X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder( 128 | new X500Name("CN=0.0.0.0, OU=None, O=None, L=None, C=None"), 129 | BigInteger.valueOf(new SecureRandom().nextInt()), new Date( 130 | System.currentTimeMillis() - 1000L * 60 * 60 * 24 131 | * 30), new Date(System.currentTimeMillis() 132 | + (1000L * 60 * 60 * 24 * 365 * 10)), new X500Name( 133 | "CN=0.0.0.0, OU=None, O=None, L=None, C=None"), 134 | SubjectPublicKeyInfo.getInstance(kp.getPublic() 135 | .getEncoded())); 136 | ContentSigner signer = new JcaContentSignerBuilder( 137 | "SHA256WithRSAEncryption").build(kp.getPrivate()); 138 | Certificate certificate = new JcaX509CertificateConverter() 139 | .getCertificate(v3CertGen.build(signer)); 140 | 141 | final KeyStore keystore = KeyStore.getInstance(KeyStore 142 | .getDefaultType()); 143 | keystore.load(null); // bogus: required to "initialize" keystore 144 | keystore.setEntry("jetty", 145 | new KeyStore.PrivateKeyEntry(kp.getPrivate(), 146 | new Certificate[] { certificate }), 147 | new KeyStore.PasswordProtection("ignored".toCharArray())); 148 | 149 | return keystore; 150 | } catch (NoSuchAlgorithmException e) { 151 | log.error( 152 | "Could not generate self-signed certificate: missing provider", 153 | e); 154 | } catch (OperatorCreationException e) { 155 | log.error("Could not generate self-signed certificate", e); 156 | } catch (CertificateException e) { 157 | log.error("Could not convert certificate to JCE", e); 158 | } catch (KeyStoreException e) { 159 | log.error("Could not generate keystore", e); 160 | } catch (IOException e) { 161 | log.error("Could not initialize keystore", e); 162 | } 163 | return null; 164 | } 165 | 166 | /** 167 | * Grabs a new server port. Borrowed from axiom. 168 | */ 169 | private int allocatePort() { 170 | try { 171 | ServerSocket ss = new ServerSocket(0); 172 | int port = ss.getLocalPort(); 173 | ss.close(); 174 | return port; 175 | } catch (IOException ex) { 176 | log.error("Unable to allocate TCP port; defaulting to 54445.", ex); 177 | return 54445; // arbitrary port 178 | } 179 | } 180 | 181 | public URL getServiceURL() { 182 | URL result = null; 183 | String protocol = secure ? "https" : "http"; 184 | try { 185 | result = new URL(protocol, "localhost", port, path); // default 186 | result = new URL(protocol, InetAddress.getLocalHost() 187 | .getHostAddress(), port, path); 188 | } catch (MalformedURLException e) { 189 | // accept default 190 | } catch (UnknownHostException e) { 191 | // accept default 192 | } 193 | return result; 194 | } 195 | 196 | public void start() { 197 | try { 198 | getJetty().start(); 199 | } catch (Exception e) { 200 | log.error("Error while starting server", e); 201 | } 202 | } 203 | 204 | public void stop() { 205 | try { 206 | server.stop(); 207 | } catch (Exception e) { 208 | log.error("Error while stopping server", e); 209 | } 210 | server.destroy(); 211 | } 212 | 213 | public ServletContextHandler getServletContextHandler() { 214 | return context; 215 | } 216 | 217 | protected ServletHolder createProvidingServletHolder() { 218 | ServletHolder servletHolder = new ServletHolder(new AbderaServlet()); 219 | servletHolder.setInitParameter( 220 | "org.apache.abdera.protocol.server.Provider", 221 | "com.trsst.server.AbderaProvider"); 222 | return servletHolder; 223 | } 224 | 225 | protected void configureContext(ServletContextHandler context) { 226 | ServletHolder servletHolder = createProvidingServletHolder(); 227 | context.addServlet(servletHolder, path + "/*"); 228 | 229 | HttpConfiguration http_config = new HttpConfiguration(); 230 | ServerConnector http = new ServerConnector(server, 231 | new HttpConnectionFactory(http_config)); 232 | http.setPort(port); 233 | 234 | if (secure) { 235 | 236 | http_config.setSecureScheme("https"); 237 | http_config.setSecurePort(8443); 238 | http_config.setOutputBufferSize(32768); 239 | 240 | final KeyStore keystore = getKeyStore(); 241 | SslContextFactory sslContextFactory = new SslContextFactory(true); 242 | sslContextFactory.setKeyStore(keystore); 243 | sslContextFactory.setCertAlias("jetty"); 244 | sslContextFactory.setKeyStorePassword("ignored"); 245 | sslContextFactory.setKeyManagerPassword("ignored"); 246 | sslContextFactory.setTrustAll(true); 247 | HttpConfiguration https_config = new HttpConfiguration(http_config); 248 | https_config.addCustomizer(new SecureRequestCustomizer()); 249 | 250 | ServerConnector https = new ServerConnector(server, 251 | new SslConnectionFactory(sslContextFactory, "http/1.1"), 252 | new HttpConnectionFactory(https_config)); 253 | https.setPort(port); 254 | server.setConnectors(new Connector[] { https }); 255 | 256 | } else { 257 | server.setConnectors(new Connector[] { http }); 258 | } 259 | } 260 | 261 | /** 262 | * Returns the jetty server. 263 | */ 264 | public org.eclipse.jetty.server.Server getJetty() { 265 | if (server == null) { 266 | server = new org.eclipse.jetty.server.Server(port); 267 | context = new ServletContextHandler( 268 | ServletContextHandler.NO_SESSIONS); 269 | context.setContextPath("/"); 270 | server.setHandler(context); 271 | configureContext(context); 272 | 273 | // FIXME: does not work: still getting session cookie 274 | // see http://stackoverflow.com/questions/20373461 275 | // ((org.eclipse.jetty.server.session.HashSessionManager) context 276 | // .getSessionHandler().getSessionManager()) 277 | // .setUsingCookies(false); 278 | } 279 | return server; 280 | } 281 | 282 | } -------------------------------------------------------------------------------- /src/main/java/com/trsst/ui/AppMain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 mpowers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.trsst.ui; 17 | 18 | import java.awt.Desktop; 19 | import java.io.File; 20 | import java.io.IOException; 21 | import java.net.URI; 22 | import java.net.URISyntaxException; 23 | import java.util.List; 24 | import java.util.logging.FileHandler; 25 | import java.util.logging.Handler; 26 | import java.util.logging.Logger; 27 | 28 | import javafx.application.Platform; 29 | import javafx.scene.Scene; 30 | import javafx.scene.layout.StackPane; 31 | import javafx.scene.web.PopupFeatures; 32 | import javafx.scene.web.WebEngine; 33 | import javafx.scene.web.WebView; 34 | import javafx.stage.Stage; 35 | import javafx.util.Callback; 36 | 37 | import com.trsst.Command; 38 | import com.trsst.Common; 39 | 40 | /** 41 | * Creates a javafx frame just to embed a real live webkit browser. This exists 42 | * as a stopgap to decouple the development of the web UI from the development 43 | * of the all-javascript client. 44 | */ 45 | public class AppMain extends javafx.application.Application { 46 | 47 | private WebView webView; 48 | private static AppleEvents appleEvents; 49 | private static String serviceUrl; 50 | 51 | public static void main(String[] argv) { 52 | 53 | if (argv.length > 0) { 54 | serviceUrl = argv[0]; 55 | } 56 | 57 | // register osx-native event handlers if needed 58 | try { 59 | // NOTE: must happen first to collect launch events 60 | appleEvents = new AppleEvents(); 61 | // major improvement in font rendering on OSX 62 | //System.setProperty("prism.lcdtext", "false"); 63 | } catch (Throwable t) { 64 | // probably wrong platform: ignore 65 | log.warn("Could not load osx events: " + t.getMessage()); 66 | } 67 | 68 | if (System.getProperty("com.trsst.server.relays") == null) { 69 | // if unspecified, default relay to home.trsst.com 70 | System.setProperty("com.trsst.server.relays", 71 | "https://home.trsst.com/feed"); 72 | } 73 | 74 | try { 75 | // write to log file if configured 76 | String path = System.getProperty("com.trsst.logfile"); 77 | if (path != null) { 78 | Handler handler = new FileHandler(path, 1024 * 1024, 3); 79 | Logger.getLogger("").addHandler(handler); 80 | log.info("Writing log file to: " + path); 81 | } 82 | } catch (SecurityException e) { 83 | log.warn("No permission to write to log file", e); 84 | } catch (IOException e) { 85 | log.warn("Can't write to log file", e); 86 | } 87 | 88 | // we connect with our local server with a self-signed certificate: 89 | // we create our server with a random port that would fail to bind 90 | // if there were a mitm that happened to be serving on that port. 91 | Common.enableAnonymousSSL(); 92 | 93 | // launch the app 94 | launch(argv); 95 | } 96 | 97 | public static AppMain instance; 98 | 99 | public static AppMain getInstance() { 100 | return instance; 101 | } 102 | 103 | @Override 104 | public void start(Stage stage) { 105 | 106 | instance = this; 107 | 108 | stage.setTitle("trsst"); 109 | webView = new WebView(); 110 | 111 | // intercept target=_blank hyperlinks 112 | webView.getEngine().setCreatePopupHandler( 113 | new Callback() { 114 | public WebEngine call(PopupFeatures config) { 115 | // grab the last hyperlink that has :hover pseudoclass 116 | Object o = webView 117 | .getEngine() 118 | .executeScript( 119 | "var list = document.querySelectorAll( ':hover' );" 120 | + "for (i=list.length-1; i>-1; i--) " 121 | + "{ if ( list.item(i).getAttribute('href') ) " 122 | + "{ list.item(i).getAttribute('href'); break; } }"); 123 | 124 | // open in native browser 125 | try { 126 | if (o != null) { 127 | Desktop.getDesktop().browse( 128 | new URI(o.toString())); 129 | } else { 130 | log.error("No result from uri detector: " + o); 131 | } 132 | } catch (IOException e) { 133 | log.error("Unexpected error obtaining uri: " + o, e); 134 | } catch (URISyntaxException e) { 135 | log.error("Could not interpret uri: " + o, e); 136 | } 137 | 138 | // prevent from opening in webView 139 | return null; 140 | } 141 | }); 142 | 143 | String url = serviceUrl; 144 | int i = url.lastIndexOf("/"); 145 | if (i != -1) { 146 | url = url.substring(0, i); 147 | } 148 | webView.getEngine().load(url); 149 | StackPane root = new StackPane(); 150 | root.getChildren().add(webView); 151 | Scene scene = new Scene(root, 900, 900); 152 | stage.setScene(scene); 153 | stage.show(); 154 | 155 | // registers osx-native event handlers 156 | // NOTE: must happen last to receive non-launch events 157 | try { 158 | appleEvents.run(); 159 | } catch (Throwable t) { 160 | // probably wrong platform: ignore 161 | log.warn("Could not start osx events: " + t.getMessage()); 162 | } 163 | } 164 | 165 | public void openURI(URI uri) { 166 | // log.info("openURI: got this far 1: " + uri); 167 | // mac feed urls use a feed:// protocol; convert to http 168 | String url = uri.toString().replace("feed://", "http://"); 169 | final String script = "controller.pushState('/" + url + "');"; 170 | // log.info("openURI: got this far 2: " + uri); 171 | Platform.runLater(new Runnable() { 172 | public void run() { 173 | try { 174 | webView.getEngine().executeScript(script); 175 | } catch (Throwable t) { 176 | log.error("Unexpected error: ", t); 177 | } 178 | // log.info("openURI: got this far 3: " + script); 179 | } 180 | }); 181 | } 182 | 183 | public void openFiles(final List files) { 184 | Platform.runLater(new Runnable() { 185 | public void run() { 186 | // for (File s : files) { 187 | // TODO: read file for path 188 | // TODO: extract link rel=self 189 | // TODO: append that link to browser location 190 | // } 191 | } 192 | }); 193 | } 194 | 195 | @Override 196 | public void stop() { 197 | System.exit(0); 198 | } 199 | 200 | private final static org.slf4j.Logger log = org.slf4j.LoggerFactory 201 | .getLogger(Command.class); 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/com/trsst/ui/AppServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 mpowers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.trsst.ui; 17 | 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.io.PrintStream; 21 | import java.nio.charset.Charset; 22 | import java.util.Date; 23 | import java.util.HashMap; 24 | import java.util.LinkedList; 25 | import java.util.List; 26 | import java.util.Map; 27 | 28 | import javax.servlet.http.HttpServlet; 29 | import javax.servlet.http.HttpServletRequest; 30 | import javax.servlet.http.HttpServletResponse; 31 | 32 | import org.apache.commons.fileupload.FileItem; 33 | import org.apache.commons.fileupload.FileUploadException; 34 | import org.apache.commons.fileupload.disk.DiskFileItemFactory; 35 | import org.apache.commons.fileupload.servlet.ServletFileUpload; 36 | import org.apache.tika.Tika; 37 | 38 | import com.trsst.Command; 39 | import com.trsst.Common; 40 | 41 | /** 42 | * Private servlet for our private client. Exposes command-line client as a 43 | * servlet for "post" command only. Parameters "attach" and "home" are rejected. 44 | * Non-local clients are rejected by default. 45 | */ 46 | public class AppServlet extends HttpServlet { 47 | 48 | private static final long serialVersionUID = -8082699767921771750L; 49 | private static final String[] ALLOWED_FILES = { "boiler.css", "index.html", 50 | "favicon.ico", "jquery-1.10.1.js", "model.js", "composer.js", 51 | "pollster.js", "renderer.js", "controller.js", "ui.css", 52 | "icon-256.png", "icon-back.png", "note.svg", "note.png", 53 | "loading-on-white.gif", "loading-on-gray.gif", 54 | "loading-on-orange.gif", "icon-rss.png" }; 55 | 56 | private final Map resources; 57 | private boolean restricted; 58 | 59 | public AppServlet() { 60 | this(true); 61 | } 62 | 63 | public AppServlet(boolean restricted) { 64 | 65 | this.restricted = restricted; 66 | 67 | // load static resources 68 | resources = new HashMap(); 69 | for (String s : ALLOWED_FILES) { 70 | try { 71 | resources.put( 72 | s, 73 | Common.readFully(this.getClass().getResourceAsStream( 74 | "site/" + s))); 75 | } catch (Exception e) { 76 | log.error("Could not load static http resource: " + s, e); 77 | } 78 | } 79 | } 80 | 81 | @Override 82 | public void doGet(HttpServletRequest request, HttpServletResponse response) 83 | throws IOException { 84 | doPost(request, response); 85 | } 86 | 87 | @Override 88 | public void doPost(HttpServletRequest request, HttpServletResponse response) 89 | throws IOException { 90 | // FLAG: limit access only to local clients 91 | if (restricted 92 | && !request.getRemoteAddr().equals(request.getLocalAddr())) { 93 | response.sendError(HttpServletResponse.SC_FORBIDDEN, 94 | "Non-local clients are not allowed."); 95 | return; 96 | } 97 | 98 | // in case of any posted files 99 | InputStream inStream = null; 100 | 101 | // determine if supported command: pull, push, post 102 | String path = request.getPathInfo(); 103 | System.err.println(new Date().toString() + " " + path); 104 | if (path != null) { 105 | // FLAG: limit only to pull and post 106 | if (path.startsWith("/pull/") || path.startsWith("/post")) { 107 | // FLAG: we're sending the user's keystore 108 | // password over the wire (over SSL) 109 | List args = new LinkedList(); 110 | if (path.startsWith("/pull/")) { 111 | path = path.substring("/pull/".length()); 112 | response.setContentType("application/atom+xml; type=feed; charset=utf-8"); 113 | // System.out.println("doPull: " + 114 | // request.getParameterMap()); 115 | args.add("pull"); 116 | if (request.getParameterMap().size() > 0) { 117 | boolean first = true; 118 | for (Object name : request.getParameterMap().keySet()) { 119 | // FLAG: don't allow "home" (server-abuse) 120 | // FLAG: don't allow "attach" (file-system access) 121 | if ("decrypt".equals(name) || "pass".equals(name)) { 122 | for (String value : request 123 | .getParameterValues(name.toString())) { 124 | args.add("--" + name.toString()); 125 | args.add(value); 126 | } 127 | } else { 128 | for (String value : request 129 | .getParameterValues(name.toString())) { 130 | if (first) { 131 | path = path + '?'; 132 | first = false; 133 | } else { 134 | path = path + '&'; 135 | } 136 | path = path + name + '=' + value; 137 | } 138 | } 139 | } 140 | } 141 | args.add(path); 142 | 143 | } else if (path.startsWith("/post")) { 144 | // System.out.println("doPost: " + 145 | // request.getParameterMap()); 146 | args.add("post"); 147 | 148 | try { // h/t http://stackoverflow.com/questions/2422468 149 | List items = new ServletFileUpload( 150 | new DiskFileItemFactory()) 151 | .parseRequest(request); 152 | for (FileItem item : items) { 153 | if (item.isFormField()) { 154 | // process regular form field 155 | String name = item.getFieldName(); 156 | String value = item.getString("UTF-8").trim(); 157 | // System.out.println("AppServlet: " + name 158 | // + " : " + value); 159 | if (value.length() > 0) { 160 | // FLAG: don't allow "home" (server-abuse) 161 | // FLAG: don't allow "attach" (file-system 162 | // access) 163 | if ("id".equals(name)) { 164 | if (value.startsWith("urn:feed:")) { 165 | value = value.substring("urn:feed:" 166 | .length()); 167 | } 168 | args.add(value); 169 | } else if (!"home".equals(name) 170 | && !"attach".equals(name)) { 171 | args.add("--" + name); 172 | args.add(value); 173 | } 174 | } else { 175 | log.debug("Empty form value for name: " 176 | + name); 177 | } 178 | } else if (item.getSize() > 0) { 179 | // process form file field (input type="file"). 180 | // String filename = FilenameUtils.getName(item 181 | // .getName()); 182 | if (item.getSize() > 1024 * 1024 * 10) { 183 | throw new FileUploadException( 184 | "Current maximum upload size is 10MB"); 185 | } 186 | String name = item.getFieldName(); 187 | if ("icon".equals(name) || "logo".equals(name)) { 188 | args.add("--" + name); 189 | args.add("-"); 190 | } 191 | inStream = item.getInputStream(); 192 | // NOTE: only handles one file! 193 | } else { 194 | log.debug("Ignored form field: " 195 | + item.getFieldName()); 196 | } 197 | } 198 | } catch (FileUploadException e) { 199 | response.sendError(HttpServletResponse.SC_BAD_REQUEST, 200 | "Could not parse multipart request: " + e); 201 | return; 202 | } 203 | } 204 | 205 | // send post data if any to command input stream 206 | if (inStream != null) { 207 | args.add("--attach"); 208 | } 209 | //System.out.println(args); 210 | 211 | // make sure we don't create another local server 212 | args.add("--host"); 213 | args.add(request.getScheme() + "://" + request.getServerName() 214 | + ":" + request.getServerPort() + "/feed"); 215 | 216 | PrintStream outStream = new PrintStream( 217 | response.getOutputStream(), false, "UTF-8"); 218 | int result = new Command().doBegin(args.toArray(new String[0]), 219 | outStream, inStream); 220 | if (result != 0) { 221 | response.sendError( 222 | HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 223 | "Internal error code: " + result); 224 | } else { 225 | outStream.flush(); 226 | } 227 | return; 228 | } 229 | 230 | // otherwise: determine if static resource request 231 | if (path.startsWith("/")) { 232 | path = path.substring(1); 233 | } 234 | 235 | byte[] result = resources.get(path); 236 | String mimetype = null; 237 | if (result == null) { 238 | // if ("".equals(path) || path.endsWith(".html")) { 239 | // treat all html requests with index doc 240 | result = resources.get("index.html"); 241 | mimetype = "text/html"; 242 | // } 243 | } 244 | if (result != null) { 245 | if (mimetype == null) { 246 | if (path.endsWith(".html")) { 247 | mimetype = "text/html"; 248 | } else if (path.endsWith(".css")) { 249 | mimetype = "text/css"; 250 | } else if (path.endsWith(".js")) { 251 | mimetype = "application/javascript"; 252 | } else if (path.endsWith(".png")) { 253 | mimetype = "image/png"; 254 | } else if (path.endsWith(".jpg")) { 255 | mimetype = "image/jpeg"; 256 | } else if (path.endsWith(".jpeg")) { 257 | mimetype = "image/jpeg"; 258 | } else if (path.endsWith(".gif")) { 259 | mimetype = "image/gif"; 260 | } else { 261 | mimetype = new Tika().detect(result); 262 | } 263 | } 264 | if (request.getHeader("If-None-Match:") != null) { 265 | // client should always use cached version 266 | log.info("sending 304"); 267 | response.setStatus(304); // Not Modified 268 | return; 269 | } 270 | // otherwise allow ETag/If-None-Match 271 | response.setHeader("ETag", Long.toHexString(path.hashCode())); 272 | if (mimetype != null) { 273 | response.setContentType(mimetype); 274 | } 275 | response.setContentLength(result.length); 276 | response.getOutputStream().write(result); 277 | return; 278 | } 279 | 280 | } 281 | 282 | // // otherwise: 404 Not Found 283 | // response.sendError(HttpServletResponse.SC_NOT_FOUND); 284 | } 285 | 286 | private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(this 287 | .getClass()); 288 | 289 | } 290 | -------------------------------------------------------------------------------- /src/main/java/com/trsst/ui/AppleEvents.java: -------------------------------------------------------------------------------- 1 | package com.trsst.ui; 2 | 3 | import com.apple.eawt.AboutHandler; 4 | import com.apple.eawt.AppEvent.AboutEvent; 5 | import com.apple.eawt.AppEvent.OpenFilesEvent; 6 | import com.apple.eawt.AppEvent.OpenURIEvent; 7 | import com.apple.eawt.AppEvent.PreferencesEvent; 8 | import com.apple.eawt.AppEvent.PrintFilesEvent; 9 | import com.apple.eawt.AppEvent.QuitEvent; 10 | import com.apple.eawt.Application; 11 | import com.apple.eawt.OpenFilesHandler; 12 | import com.apple.eawt.OpenURIHandler; 13 | import com.apple.eawt.PreferencesHandler; 14 | import com.apple.eawt.PrintFilesHandler; 15 | import com.apple.eawt.QuitHandler; 16 | import com.apple.eawt.QuitResponse; 17 | 18 | /** 19 | * Custom handlers for OS X launch services; allows us to be registered as an 20 | * system feed reader. 21 | * 22 | * @author mpowers 23 | * 24 | */ 25 | public class AppleEvents implements OpenURIHandler, OpenFilesHandler, 26 | PreferencesHandler, PrintFilesHandler, QuitHandler, AboutHandler, 27 | Runnable { 28 | 29 | public AppleEvents() { 30 | // NOTE: need to invoke application here to collect launch events 31 | Application.getApplication().setDockIconBadge("..."); 32 | } 33 | 34 | public void run() { 35 | // NOTE: if these are invoked earlier, jfx disrupts them or something 36 | Application.getApplication().setDockIconBadge(""); 37 | Application.getApplication().setOpenURIHandler(this); 38 | Application.getApplication().setOpenFileHandler(this); 39 | log.info("Now receiving OSX events"); 40 | } 41 | 42 | private final static org.slf4j.Logger log = org.slf4j.LoggerFactory 43 | .getLogger(AppleEvents.class); 44 | 45 | public void openURI(OpenURIEvent e) { 46 | // Application.getApplication().setDockIconBadge("URI"); 47 | log.info("openURI: " + e.getURI()); 48 | AppMain.getInstance().openURI(e.getURI()); 49 | 50 | } 51 | 52 | public void handleAbout(AboutEvent e) { 53 | log.info("handleAbout: "); 54 | // Application.getApplication().setDockIconBadge("About"); 55 | } 56 | 57 | public void handleQuitRequestWith(QuitEvent e, QuitResponse arg1) { 58 | log.info("handleQuitRequestWith: "); 59 | // Application.getApplication().setDockIconBadge("Quit"); 60 | } 61 | 62 | public void printFiles(PrintFilesEvent e) { 63 | log.info("printFiles: "); 64 | // Application.getApplication().setDockIconBadge("Print"); 65 | } 66 | 67 | public void handlePreferences(PreferencesEvent e) { 68 | log.info("handlePreferences: "); 69 | // Application.getApplication().setDockIconBadge("Prefs"); 70 | } 71 | 72 | public void openFiles(OpenFilesEvent e) { 73 | // Application.getApplication().setDockIconBadge("File"); 74 | log.info("openFiles: " + e.getSearchTerm() + " : " + e.getFiles()); 75 | AppMain.getInstance().openFiles(e.getFiles()); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/boiler.css: -------------------------------------------------------------------------------- 1 | /* @override 2 | http://localhost:8181/boiler.css */ 3 | 4 | /* ========================================================================== 5 | HTML5 Boilerplate styles - h5bp.com (generated via initializr.com) 6 | ========================================================================== */ 7 | 8 | html, 9 | button, 10 | input, 11 | select, 12 | textarea { 13 | color: #222; 14 | } 15 | 16 | body { 17 | font-size: 1em; 18 | line-height: 1.4; 19 | } 20 | 21 | ::-moz-selection { 22 | background: #b3d4fc; 23 | text-shadow: none; 24 | } 25 | 26 | ::selection { 27 | background: #b3d4fc; 28 | text-shadow: none; 29 | } 30 | 31 | hr { 32 | display: block; 33 | height: 1px; 34 | border: 0; 35 | border-top: 1px solid #ccc; 36 | margin: 1em 0; 37 | padding: 0; 38 | } 39 | 40 | img { 41 | vertical-align: middle; 42 | } 43 | 44 | fieldset { 45 | border: 0; 46 | margin: 0; 47 | padding: 0; 48 | } 49 | 50 | textarea { 51 | resize: vertical; 52 | } 53 | 54 | .chromeframe { 55 | margin: 0.2em 0; 56 | background: #ccc; 57 | color: #000; 58 | padding: 0.2em 0; 59 | } 60 | 61 | 62 | /* ===== Initializr Styles ================================================== 63 | Author: Jonathan Verrecchia - verekia.com/initializr/responsive-template 64 | ========================================================================== */ 65 | 66 | body { 67 | font: 16px/26px Helvetica, Helvetica Neue, Arial; 68 | } 69 | 70 | .wrapper { 71 | width: 90%; 72 | margin: 0 5%; 73 | } 74 | 75 | /* =================== 76 | ALL: Orange Theme 77 | =================== */ 78 | 79 | .header-container { 80 | border-bottom: 20px solid #e44d26; 81 | } 82 | 83 | .footer-container, 84 | .main aside { 85 | border-top: 20px solid #e44d26; 86 | } 87 | 88 | .header-container, 89 | .footer-container, 90 | .main aside { 91 | background: #f16529; 92 | } 93 | 94 | .title { 95 | color: white; 96 | } 97 | 98 | /* ============== 99 | MOBILE: Menu 100 | ============== */ 101 | 102 | nav ul { 103 | margin: 0; 104 | padding: 0; 105 | } 106 | 107 | nav a { 108 | display: block; 109 | margin-bottom: 10px; 110 | padding: 15px 0; 111 | 112 | text-align: center; 113 | text-decoration: none; 114 | font-weight: bold; 115 | 116 | color: white; 117 | background: #e44d26; 118 | } 119 | 120 | nav a:hover, 121 | nav a:visited { 122 | color: white; 123 | } 124 | 125 | nav a:hover { 126 | text-decoration: underline; 127 | } 128 | 129 | /* ============== 130 | MOBILE: Main 131 | ============== */ 132 | 133 | .main { 134 | padding: 30px 0; 135 | } 136 | 137 | .main article h1 { 138 | font-size: 2em; 139 | } 140 | 141 | .main aside { 142 | color: white; 143 | padding: 0px 0px 10px; 144 | } 145 | 146 | .footer-container footer { 147 | color: white; 148 | padding: 20px 0; 149 | } 150 | 151 | /* =============== 152 | ALL: IE Fixes 153 | =============== */ 154 | 155 | .ie7 .title { 156 | padding-top: 20px; 157 | } 158 | 159 | /* ========================================================================== 160 | Author's custom styles 161 | ========================================================================== */ 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | /* ========================================================================== 178 | Media Queries 179 | ========================================================================== */ 180 | 181 | @media only screen and (min-width: 480px) { 182 | 183 | /* ==================== 184 | INTERMEDIATE: Menu 185 | ==================== */ 186 | 187 | nav a { 188 | float: left; 189 | width: 27%; 190 | margin: 0 1.7%; 191 | padding: 25px 2%; 192 | margin-bottom: 0; 193 | } 194 | 195 | nav li:first-child a { 196 | margin-left: 0; 197 | } 198 | 199 | nav li:last-child a { 200 | margin-right: 0; 201 | } 202 | 203 | /* ======================== 204 | INTERMEDIATE: IE Fixes 205 | ======================== */ 206 | 207 | nav ul li { 208 | display: inline; 209 | } 210 | 211 | .oldie nav a { 212 | margin: 0 0.7%; 213 | } 214 | } 215 | 216 | @media only screen and (min-width: 768px) { 217 | 218 | /* ==================== 219 | WIDE: CSS3 Effects 220 | ==================== */ 221 | 222 | .header-container, 223 | .main aside { 224 | -webkit-box-shadow: 0 5px 10px #aaa; 225 | -moz-box-shadow: 0 5px 10px #aaa; 226 | box-shadow: 0 5px 10px #aaa; 227 | } 228 | 229 | /* ============ 230 | WIDE: Menu 231 | ============ */ 232 | 233 | .title { 234 | float: left; 235 | } 236 | 237 | nav { 238 | float: right; 239 | width: 38%; 240 | } 241 | 242 | /* ============ 243 | WIDE: Main 244 | ============ */ 245 | 246 | .main article { 247 | float: right; 248 | width: 58%; 249 | } 250 | 251 | .main aside { 252 | float: left; 253 | width: 40%; 254 | } 255 | } 256 | 257 | @media only screen and (min-width: 1140px) { 258 | 259 | /* =============== 260 | Maximal Width 261 | =============== */ 262 | 263 | .wrapper { 264 | width: 1026px; /* 1140px - 10% for margins */ 265 | margin: 0 auto; 266 | } 267 | } 268 | 269 | /* ========================================================================== 270 | Helper classes 271 | ========================================================================== */ 272 | 273 | .ir { 274 | background-color: transparent; 275 | border: 0; 276 | overflow: hidden; 277 | *text-indent: -9999px; 278 | } 279 | 280 | .ir:before { 281 | content: ""; 282 | display: block; 283 | width: 0; 284 | height: 150%; 285 | } 286 | 287 | .hidden { 288 | display: none !important; 289 | visibility: hidden; 290 | } 291 | 292 | .visuallyhidden { 293 | border: 0; 294 | clip: rect(0 0 0 0); 295 | height: 1px; 296 | margin: -1px; 297 | overflow: hidden; 298 | padding: 0; 299 | position: absolute; 300 | width: 1px; 301 | } 302 | 303 | .visuallyhidden.focusable:active, 304 | .visuallyhidden.focusable:focus { 305 | clip: auto; 306 | height: auto; 307 | margin: 0; 308 | overflow: visible; 309 | position: static; 310 | width: auto; 311 | } 312 | 313 | .invisible { 314 | visibility: hidden; 315 | } 316 | 317 | .clearfix:before, 318 | .clearfix:after { 319 | content: " "; 320 | display: table; 321 | } 322 | 323 | .clearfix:after { 324 | clear: both; 325 | } 326 | 327 | .clearfix { 328 | *zoom: 1; 329 | } 330 | 331 | /* ========================================================================== 332 | Print styles 333 | ========================================================================== */ 334 | 335 | @media print { 336 | * { 337 | background: transparent !important; 338 | color: #000 !important; /* Black prints faster: h5bp.com/s */ 339 | box-shadow: none !important; 340 | text-shadow: none !important; 341 | } 342 | 343 | a, 344 | a:visited { 345 | text-decoration: underline; 346 | } 347 | 348 | a[href]:after { 349 | content: " (" attr(href) ")"; 350 | } 351 | 352 | abbr[title]:after { 353 | content: " (" attr(title) ")"; 354 | } 355 | 356 | /* 357 | * Don't show links for images, or javascript/internal links 358 | */ 359 | 360 | .ir a:after, 361 | a[href^="javascript:"]:after, 362 | a[href^="#"]:after { 363 | content: ""; 364 | } 365 | 366 | pre, 367 | blockquote { 368 | border: 1px solid #999; 369 | page-break-inside: avoid; 370 | } 371 | 372 | thead { 373 | display: table-header-group; /* h5bp.com/t */ 374 | } 375 | 376 | tr, 377 | img { 378 | page-break-inside: avoid; 379 | } 380 | 381 | img { 382 | max-width: 100% !important; 383 | } 384 | 385 | @page { 386 | margin: 0.5cm; 387 | } 388 | 389 | p, 390 | h2, 391 | h3 { 392 | orphans: 3; 393 | widows: 3; 394 | } 395 | 396 | h2, 397 | h3 { 398 | page-break-after: avoid; 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/composer.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2014 mpowers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | (function(window, undefined) { 17 | 18 | /** 19 | * Composer manages an entry composer form, submitting its contents to the 20 | * model when needed. Variants can just replace or extend this object. If a 21 | * renderer is specified, new entries will be inserted into it. 22 | */ 23 | Composer = window.Composer = function(form, renderers) { 24 | this.form = $(form); 25 | this.renderers = renderers; 26 | var self = this; 27 | this.form.submit(function(e) { 28 | e.preventDefault(); 29 | self.onSubmit(); 30 | }); 31 | this.form.find(".submit").click(function(e) { 32 | e.preventDefault(); 33 | self.form.submit(); 34 | }); 35 | 36 | var attach = this.form.find(".attach"); 37 | var input = this.form.find("input[type='file']"); 38 | attach.click(function(e) { 39 | e.preventDefault(); 40 | input[0].value = ""; // clear any preexisting value 41 | input.change(); 42 | input.click(); 43 | }); 44 | input.on('change', function(e) { 45 | // grab file name so the UI can display it 46 | var value = input[0].value; 47 | if (value !== undefined) { 48 | var i = value.lastIndexOf('\\'); 49 | if (i !== -1) { 50 | value = value.substring(i + 1); 51 | } 52 | attach.attr("file", value); 53 | } 54 | }); 55 | }; 56 | Composer.prototype = new Composer(); 57 | Composer.prototype.constructor = Composer; 58 | 59 | /** 60 | * Adds feed to container, replacing any existing element for this feed. 61 | * Returns the added feed element. 62 | */ 63 | Composer.prototype.onSubmit = function() { 64 | console.log("onSubmit: "); 65 | var value = this.form.find("textarea").val().trim(); 66 | if ("" === value) { 67 | console.log("Not posting because nothing to send."); 68 | } else { 69 | var self = this; 70 | var formData = new FormData(self.form[0]); 71 | 72 | // determine public or private 73 | var encrypted = this.form.find("select[name='encrypt'] option:selected").hasClass("private"); 74 | var entry = this.form.closest(".entry"); 75 | 76 | if (entry.length === 1) { 77 | // if replying to an encrypted entry 78 | if (!encrypted && entry.hasClass("content-decrypted")) { 79 | var privateOption = this.form.find("select[name='encrypt'] option.private"); 80 | // force this entry to be encrypted for all mentions 81 | formData.append("encrypt", "-"); 82 | } 83 | } 84 | 85 | // copy mentions from enclosed reply 86 | var i; 87 | if (entry.length === 1) { 88 | 89 | // determine if we're sharing 90 | // but default to replying 91 | var entryUrn = entry.attr("entry"); 92 | if (entry.hasClass("sharing")) { 93 | formData.append("verb", "share"); 94 | } else { 95 | formData.append("verb", "reply"); 96 | } 97 | 98 | // copy any primary discussion parent posts first 99 | entry.find(".addresses .parent").each(function() { 100 | var text = $(this).attr("title"); 101 | text = model.entryUrnFromEntryId(text); 102 | formData.append("mention", text); 103 | }); 104 | 105 | // mention the current parent post last 106 | formData.append("mention", entryUrn); 107 | 108 | // mention the author if it's not us 109 | var nodupes = []; 110 | var authorId = model.feedIdFromEntryUrn(entryUrn); 111 | var authorUrn = model.feedUrnFromFeedId(authorId); 112 | if (authorUrn !== model.getAuthenticatedAccountId()) { 113 | // check to see if we have an alias for this user 114 | var feed = model.getFeed(authorUrn); 115 | if (feed) { 116 | var aliasUrn = $(feed).find("author uri").text(); 117 | if (aliasUrn) { 118 | // use the alias instead 119 | authorUrn = model.getMentionForAliasAndFeedUrn(aliasUrn, authorId); 120 | } 121 | } 122 | nodupes.push(authorUrn); 123 | } 124 | 125 | // copy any mentions (excluding ourself) 126 | entry.find(".addresses .mention").each(function() { 127 | var text = $(this).find("span").text(); 128 | if (text.indexOf('@') === 0) { 129 | text = text.substring(1); 130 | } 131 | text = model.feedUrnFromFeedId(text); 132 | if (text !== model.getAuthenticatedAccountId()) { 133 | if (nodupes.indexOf(text) === -1) { 134 | nodupes.push(text); 135 | } 136 | } 137 | }); 138 | 139 | // create the mentions 140 | for (i in nodupes) { 141 | formData.append("mention", nodupes[i]); 142 | } 143 | 144 | // copy any tags 145 | entry.find(".addresses .tag").each(function() { 146 | var text = $(this).find("span").text(); 147 | if (text.indexOf('#') === 0) { 148 | text = text.substring(1); 149 | } 150 | formData.append("tag", text); 151 | }); 152 | 153 | } 154 | 155 | // find tags and mentions in the text 156 | var match; 157 | var matches = value.match(/([\@\#]\w*)/g); 158 | if (matches) { 159 | for (i in matches) { 160 | match = matches[i]; 161 | if (match.charAt(0) === '@') { 162 | match = match.substring(1); 163 | var feeds = model.findFollowedFeedsMatching(match); 164 | // if we matched exactly one 165 | if (feeds.length === 1) { 166 | // create a mention 167 | var alias = $(feeds[0]).find("author>uri").text(); 168 | var id = $(feeds[0]).children("id").text(); 169 | 170 | if (id) { 171 | if (alias) { 172 | id = model.getMentionForAliasAndFeedUrn(alias, id); 173 | } 174 | formData.append("mention", id); 175 | } else { 176 | console.log("Could not match mention: " + match); 177 | console.log(feeds[0]); 178 | } 179 | // FIXME: until we get our js address checker 180 | } else if (match.length > 25 && match.length < 35) { 181 | // assume a mention of that size is an account id 182 | formData.append("mention", "urn:feed:" + match); 183 | // TODO: assume an alias on our home domain 184 | } 185 | } else { 186 | // otherwise hash->tag 187 | formData.append("tag", match.substring(1)); 188 | } 189 | } 190 | } 191 | 192 | var gruberUrlg = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/ig; 193 | matches = value.match(gruberUrlg); 194 | if (matches) { 195 | for (i in matches) { 196 | formData.append("url", matches[i]); 197 | } 198 | } 199 | 200 | self.form.addClass("pending"); 201 | self.form.find("button").attr("disabled", "disabled"); 202 | model.updateFeed(formData, function(feedData) { 203 | self.form.removeClass("pending"); 204 | self.form.find("button").removeAttr("disabled"); 205 | if (feedData) { 206 | // success 207 | console.log("updateFeed: result: "); 208 | console.log(feedData); 209 | self.form[0].reset(); 210 | self.form.find(".attach").removeAttr("file"); 211 | self.form.removeClass("error"); 212 | controller.forceRender(feedData); 213 | if (self.renderers) { 214 | // update the ui immediately for better ux 215 | for ( var renderer in self.renderers) { 216 | self.renderers[renderer].addEntriesFromFeed(feedData); 217 | } 218 | } 219 | } else { 220 | // error 221 | self.form.addClass("error"); 222 | } 223 | }); 224 | } 225 | }; 226 | 227 | /** 228 | * Clears all tags and mentions from this composer. 229 | */ 230 | Composer.prototype.clearAddresses = function() { 231 | this.form.find(".addresses>span").empty(); 232 | }; 233 | 234 | /** 235 | * Adds a tag to this composer. 236 | */ 237 | Composer.prototype.addTag = function(address) { 238 | address = address.toString().toLowerCase(); 239 | var e = $(""); 240 | e.attr("href", '/?tag=' + address); 241 | e.attr("title", address); 242 | e.children("span").text('#' + address); 243 | this.form.find(".addresses>span").append(e); 244 | }; 245 | 246 | /** 247 | * Adds a mention to this composer. 248 | */ 249 | Composer.prototype.addMention = function(address) { 250 | var urn = model.feedUrnFromFeedId(address); 251 | var e = $(""); 252 | e.attr("href", '/?mention=' + address); 253 | e.attr("title", address); 254 | e.children("span").text('@' + address); 255 | this.form.find(".addresses>span").append(e); 256 | }; 257 | 258 | })(window); 259 | -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/resources/com/trsst/ui/site/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/resources/com/trsst/ui/site/icon-256.png -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/icon-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/resources/com/trsst/ui/site/icon-back.png -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/icon-rss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/resources/com/trsst/ui/site/icon-rss.png -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Trsst 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 35 |

36 | trsst 38 |

39 | 76 |
77 |
78 | 79 | 82 | 83 |
84 |
85 | 86 | 118 | 119 |
120 |
121 | 122 | 123 |
124 | 125 |
126 | 129 | 130 | 133 | 137 |
138 |
139 |
140 |
141 |
142 | 143 |
144 | 145 |
146 | 147 | 148 | 165 | 166 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/loading-on-gray.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/resources/com/trsst/ui/site/loading-on-gray.gif -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/loading-on-orange.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/resources/com/trsst/ui/site/loading-on-orange.gif -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/loading-on-white.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/resources/com/trsst/ui/site/loading-on-white.gif -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/resources/com/trsst/ui/site/note.png -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/note.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/com/trsst/ui/site/pollster.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2014 mpowers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | (function(window, undefined) { 17 | 18 | /* 19 | * Pollster monitors feeds and notifies subscribers of updates. We expect to 20 | * see a number of different strategies for achieving near-real-time 21 | * results, and those should be drop in replacements for this interface. 22 | */ 23 | var pollster = window.pollster = {}; 24 | 25 | /** 26 | * Shared timer. 27 | */ 28 | var timer; 29 | 30 | /** 31 | * Ordered queue of tasks to complete, highest priority at front. 32 | */ 33 | var queue = []; 34 | 35 | /** 36 | * Shared callback list. 37 | */ 38 | var topicToSubscribers = {}; 39 | 40 | /** 41 | * Mapping of topic to corresponding task. 42 | */ 43 | var topicToTask = {}; 44 | 45 | /** 46 | * Keeps a copy of latest result for a given query. TODO: need to clear out 47 | * the cache after a while 48 | */ 49 | var resultCache = {}; 50 | 51 | /** 52 | * Subscribe to have your notify(feedXml, query) method called each time new 53 | * results are received for the specified query. 54 | */ 55 | pollster.subscribe = function(query, subscriber) { 56 | var topic = JSON.stringify(query); 57 | var subscribers = topicToSubscribers[topic]; 58 | if (subscribers) { 59 | var existing = subscribers.indexOf(subscriber); 60 | if (existing === -1) { 61 | subscribers.push(subscriber); 62 | } 63 | } else { 64 | topicToSubscribers[topic] = [ subscriber ]; 65 | } 66 | 67 | // if existing task 68 | var task = topicToTask[topic]; 69 | if (task) { 70 | if (task.latestFeed) { 71 | // notify subscriber now of latest results 72 | subscriber.notify(task.latestFeed, query); 73 | } else { 74 | // trigger refetch asap 75 | task.noFetchBefore = 0; 76 | } 77 | // will get updated on already queued task 78 | } else { 79 | // otherwise: create new task 80 | task = { 81 | query : query, 82 | lastUpdate : 0, 83 | lastFetched : 0, 84 | noFetchBefore : 0 85 | }; 86 | topicToTask[topic] = task; 87 | // execute now 88 | doTask(task); 89 | // doTask will insert task into queue 90 | } 91 | 92 | }; 93 | 94 | pollster.unsubscribe = function(subscriber) { 95 | // scan the entire list and remove ourself 96 | for ( var i in topicToSubscribers) { 97 | for ( var j in topicToSubscribers[i]) { 98 | if (topicToSubscribers[i][j] === subscriber) { 99 | topicToSubscribers[i].splice(j, 1); // remove 100 | if (topicToSubscribers[i].length === 0) { 101 | delete topicToSubscribers[i]; 102 | } 103 | } 104 | } 105 | } 106 | }; 107 | 108 | var concurrentFetchCount = 0; 109 | 110 | pollster.getPendingCount = function() { 111 | return concurrentFetchCount; 112 | }; 113 | 114 | pollster.incrementPendingCount = function() { 115 | concurrentFetchCount++; 116 | // if (concurrentFetchCount > 2) { 117 | if (concurrentFetchCount > 0) { // too much? 118 | $("body").addClass("pending"); 119 | } 120 | }; 121 | 122 | pollster.decrementPendingCount = function() { 123 | concurrentFetchCount--; 124 | if (concurrentFetchCount === 0) { 125 | $("body").removeClass("pending"); 126 | } 127 | }; 128 | 129 | /** 130 | * Executes the task's query and notifies subscribers. Return true if task 131 | * completed successfully, or false if no query was executed. 132 | */ 133 | var doTask = function(task) { 134 | var topic = JSON.stringify(task.query); 135 | var subscribers = topicToSubscribers[topic]; 136 | if (!subscribers || subscribers.length === 0) { 137 | // console.log("Deleting task: " + topic); 138 | // console.log(task); 139 | // delete topicToSubscribers[topic]; 140 | // delete topicToTask[topic]; 141 | return false; // task skipped 142 | } 143 | // console.log("doTask: " + task.toString()); 144 | var query = shallowCopy(task.query); 145 | var feedId = query.feedId; 146 | 147 | if (task.latestEntryId) { 148 | // use latest entry update time 149 | query.after = task.latestEntryId; 150 | } 151 | 152 | // if first time executing task 153 | if (task.lastFetched === 0) { 154 | // fetch only latest few and requeue 155 | query.count = 4; 156 | } 157 | 158 | var self = this; 159 | pollster.incrementPendingCount(); 160 | console.log("concurrentFetchCount: inc:" + pollster.getPendingCount()); 161 | var stringifiedQuery = JSON.stringify(query); 162 | console.log("Sent: " + pollster.getPendingCount() + " : " + stringifiedQuery); 163 | var currentTask = task; 164 | model.pull(query, function(feedData) { 165 | if (!feedData) { 166 | console.log("Not found: " + pollster.getPendingCount() + " : " + JSON.stringify(query)); 167 | } else { 168 | // console.log("Received: " + pollster.getPendingCount() + " : " 169 | // + JSON.stringify(query)); 170 | 171 | // call each subscriber's notify function 172 | for ( var i in subscribers) { 173 | subscribers[i].notify(feedData, query); 174 | } 175 | 176 | // grab the latest result if any 177 | resultCache[stringifiedQuery] = feedData; 178 | var entries = feedData.children("entry"); 179 | if (entries.length > 0) { 180 | currentTask.latestEntry = entries.first(); 181 | currentTask.latestEntryId = model.entryIdFromEntryUrn(currentTask.latestEntry.children("id").text()); 182 | currentTask.latestFeed = feedData; 183 | // remember latest feed *with entries* 184 | } 185 | 186 | // requeue this task 187 | var now = new Date().getTime(); 188 | var updated; 189 | var diff; 190 | if (currentTask.latestEntry) { 191 | // use latest entry update if we can 192 | updated = Date.parse(currentTask.latestEntry.find("entry updated").first().text()); 193 | if (!updated) { 194 | console.log("Error: could not parse entry date: " + Date.parse($(feedData).children("entry updated").text())); 195 | } 196 | } else { 197 | // fall back on feed's updated date 198 | updated = Date.parse(feedData.children("updated").text()); 199 | if (!updated) { 200 | console.log("Error: could not parse feed date: " + feedData.children("updated").text()); 201 | } 202 | } 203 | if (!updated) { 204 | console.log(" defaulting to one hour"); 205 | diff = 60 * 60 * 1000; // default to 1 hour 206 | updated = now - diff; 207 | } 208 | diff = now - updated; 209 | if (diff < 1) { 210 | console.log("Error: feed was updated in the future: " + updated); 211 | diff = 60 * 60 * 1000; // default to 1 hour 212 | } 213 | 214 | if (currentTask.lastFetched === 0) { 215 | // first time fetch: 216 | // fetch again after 15 seconds to allow for relays to 217 | // respond 218 | currentTask.noFetchBefore = now + 15 * 1000; 219 | console.log("rescheduled: " + currentTask.query.feedId + " : asap"); 220 | } else { 221 | // fetch on a sliding delay 222 | diff = Math.max(6, Math.min(diff, Math.floor(Math.pow(diff / 60000, 1 / 3) * 20000))); 223 | currentTask.noFetchBefore = now + diff; 224 | // schedule fetch for cube root of the number of elapsed 225 | // minutes 226 | console.log("rescheduled: " + currentTask.query.feedId + " : " + Math.floor((now - updated) / 1000) + "s : " + Math.floor(diff / 1000 / 60) + "m " + Math.floor((diff / 1000) % 60) + "s"); 227 | } 228 | currentTask.lastUpdate = updated; 229 | currentTask.lastFetched = now; 230 | 231 | insertTaskIntoSortedQueue(currentTask); 232 | } 233 | pollster.decrementPendingCount(); 234 | console.log("concurrentFetchCount: dec:" + pollster.getPendingCount()); 235 | 236 | }); 237 | return true; // task was handled 238 | }; 239 | 240 | var insertTaskIntoSortedQueue = function(task) { 241 | // console.log("insertTaskIntoSortedQueue: "); 242 | // console.log(task); 243 | // could try binary search but suspect 244 | // reverse linear might be faster with js array: 245 | var time = task.nextFetch; 246 | var next; 247 | for (var i = queue.length - 1; i >= 0; i--) { 248 | next = queue[i].nextFetch; 249 | if (next === time) { 250 | // check for duplicate task 251 | if (JSON.stringify(queue[i].query) === JSON.stringify(task.query)) { 252 | console.log("Coalescing duplicate task"); 253 | return; // done: exit 254 | } 255 | } 256 | if (next < time) { 257 | queue.splice(i + i, 0, task); 258 | return; // done: exit 259 | } 260 | } 261 | // insert at rear of queue 262 | queue.splice(0, 0, task); 263 | }; 264 | 265 | /** 266 | * Resumes polling. 267 | */ 268 | var start = function() { 269 | if (!timer) { 270 | timer = window.setInterval(function() { 271 | onTick(); 272 | }, 1000); 273 | } 274 | }; 275 | 276 | /** 277 | * Pauses polling. 278 | */ 279 | var stop = function() { 280 | window.clearInterval(timer); 281 | }; 282 | 283 | /** 284 | * Called with each tick of the timer to refetch any pending feeds on the 285 | * queue. 286 | */ 287 | var onTick = function() { 288 | if (pollster.getPendingCount() < 5) { 289 | // console.log("onTick"); 290 | var task; 291 | var time = new Date().getTime(); 292 | for (var i = queue.length - 1; i >= 0; i--) { 293 | task = queue[i]; 294 | if (task.noFetchBefore < time) { 295 | queue.splice(i, 1); // remove 296 | if (doTask(task)) { 297 | return; // done: exit 298 | } 299 | } 300 | } 301 | } 302 | }; 303 | 304 | var shallowCopy = function(obj) { 305 | var result = {}; 306 | for ( var i in obj) { 307 | result[i] = obj[i]; 308 | } 309 | return result; 310 | }; 311 | 312 | /** 313 | * Called by model to notify us of a local change to a feed so we can 314 | * refresh our subscribers if needed. 315 | */ 316 | model.subscribe(function(feedId) { 317 | var copy = []; 318 | var priority = []; 319 | var task; 320 | var i; 321 | for (i in queue) { 322 | task = queue[i]; 323 | // catch plain and urn:feed case 324 | if (feedId.indexOf(task.query.feedId) != -1) { 325 | // fetch asap 326 | task.noFetchBefore = 0; 327 | task.lastUpdate = new Date().getTime(); 328 | priority.push(task); 329 | } else { 330 | copy.push(task); 331 | } 332 | } 333 | for (i in priority) { 334 | copy.push(priority[i]); 335 | } 336 | queue = copy; 337 | }); 338 | 339 | /** 340 | * Start the timer. 341 | */ 342 | start(); 343 | 344 | })(window); 345 | -------------------------------------------------------------------------------- /src/main/resources/eawt.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrsstProject/trsst/338214dd95952d45898f772ed9dfc4fdb1fc425c/src/main/resources/eawt.jar --------------------------------------------------------------------------------