├── MIT-LICENSE ├── README.md ├── cubiti-server ├── Dockerfile ├── cli.js ├── cmd.js ├── config.js ├── db.js ├── entrypoint.sh ├── package.json ├── public │ ├── .DS_Store │ ├── css │ │ └── main.css │ ├── images │ │ ├── cubiti-logo-cropped.png │ │ ├── cubiti-logo-icon.png │ │ └── cubiti-logo.png │ └── index.html ├── routes │ ├── activitypub.js │ └── mastodon.js ├── server.js ├── sysevents.js ├── util.js ├── views │ └── login.ejs └── websock.js ├── data └── config.json ├── docker-compose.yaml └── nginx-alpine-ssl ├── Dockerfile ├── default.conf ├── entrypoint.sh ├── nginx-selfsigned.crt └── nginx-selfsigned.key /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Toby Jaffey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | cubiti 3 |

4 | 5 | cubiti is a toy Fediverse server written in Node.js 6 |
7 | It speaks to Mastodon and other federated network services supporting [ActivityPub](https://www.w3.org/TR/activitypub/) 8 | 9 | As a front-end, it provides a partial implementation of the [Mastodon API](https://docs.joinmastodon.org/client/intro/) so you can connect with Mastodon client apps such as [Pinafore](https://pinafore.social/), [MetaText](https://apps.apple.com/us/app/metatext/id1523996615), [Mammoth](https://mastodon.social/@JPEGuin/109315609418460036) and more. 10 | 11 | cubiti serves two APIs, Mastodon in the front and ActivityPub in the back. 12 | 13 | Developing cubiti was an exercise in learning about Fediverse APIs and protocols. As a service, **it is insecure, buggy and incomplete**. However, it supports enough to follow, post, like, reblog and view conversations including image media, blurhash and content warnings. 14 | 15 | Note, the admin command interface is more mature than the Mastodon API support. 16 | 17 | ## Tech stack 18 | 19 | - **sqlite3** (persistent storage) 20 | - **redis** (pubsub for realtime timeline updates) 21 | - **Node.js** (everything else) 22 | 23 | ## Initial setup 24 | 25 | docker-compose build 26 | docker-compose up 27 | 28 | Create a user account via the admin command interface 29 | 30 | docker-compose run --rm cubiti-server 31 | 32 | At the prompt, type: 33 | 34 | user add cubiti mypassword 35 | 36 | It will respond by showing part of the created database record: 37 | 38 | { 39 | userid: 1, 40 | username: 'cubiti', 41 | pubkey: '-----BEGIN PUBLIC KEY-----\n' + 42 | ... 43 | } 44 | 45 | Browse to [https://localhost/](https://localhost/) (and accept the security warning about the self-signed certificate mismatch). Click the invitation to login via [pinafore.social](https://pinafore.social) 46 | 47 | You should see an empty timeline. Some interaction is possible, but you are talking to yourself. As the @cubiti users does not follow anyone, posted messages will be dropped. 48 | 49 | ## Setup a real server 50 | 51 | - Buy a domain, configure DNS, create a valid key pair 52 | - Copy the cert over `nginx-alpine-ssl/nginx-selfsigned.crt` and the key over `nginx-alpine-ssl/nginx-selfsigned.key` 53 | - Edit `data/config.json` and set `domain` to your real domain 54 | 55 | Run the server 56 | 57 | docker-compose down 58 | docker-compose build 59 | docker-compose up 60 | 61 | Test that your @cubiti account is discoverable with Webfinger 62 | 63 | - Go to [https://webfinger.net/](https://webfinger.net/) 64 | - Type in `cubiti@yourdomain` 65 | - You should see a JSON webfinger document 66 | 67 | Browse to https://domain, follow the link to Pinafore, add `domain` and login. You should now see an empty timeline. 68 | 69 | Well done! You're on the fediverse 70 | 71 | ## Next 72 | 73 | The search Mastodon API is not yet implemented, so you have nobody to follow and no posts to view yet. 74 | 75 | Either, use another fediverse account to start interacting with cubiti@domain, or use the admin interface 76 | 77 | To get a prompt, run: 78 | 79 | docker-compose run --rm cubiti-server 80 | 81 | Most command require a `userid`, that's the user the admin is executing the command for. If you only have the single user we created above, use `1` 82 | 83 | At the prompt, you can follow a user with: 84 | 85 | action follow 1 https://mastodon.me.uk/users/tobyjaffey 86 | 87 | To send a message to all of your followers: 88 | 89 | action sendnote 1 "Hello world" 90 | 91 | To unfollow: 92 | 93 | action unfollow 1 https://mastodon.me.uk/users/tobyjaffey 94 | 95 | To like a post: 96 | 97 | action like 1 https://foo.social/users/bar/statuses/109548408290883351 https://foo.social/users/bar 98 | 99 | To unlike a post 100 | 101 | action unlike 1 https://foo.social/users/bar/statuses/109548408290883351 https://foo.social/users/bar 102 | 103 | ## Next next 104 | 105 | Now that you have some followers and some data, you can do the same actions (and more) through the Mastodon API (via Pinafore). Some things work, some do not. 106 | 107 | ## Contact 108 | 109 | @tobyjaffey@mastodon.me.uk 110 | 111 | [https://mastodon.me.uk/users/tobyjaffey](mastodon.me.uk/users/tobyjaffey) 112 | 113 | -------------------------------------------------------------------------------- /cubiti-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | ENV LANG C.UTF-8 3 | ENV LC_ALL C.UTF-8 4 | #RUN apk add --no-cache curl-dev build-base 5 | #RUN make 6 | 7 | # use changes to package.json to force Docker not to use the cache 8 | # when we change our application's nodejs dependencies: 9 | RUN mkdir -p /build 10 | COPY package.json /tmp/package.json 11 | RUN cd /tmp && npm install 12 | RUN cp -a /tmp/node_modules /build 13 | 14 | COPY . /build 15 | WORKDIR /build 16 | 17 | COPY entrypoint.sh /usr/local/bin/entrypoint.sh 18 | RUN chmod +x /usr/local/bin/entrypoint.sh 19 | ENTRYPOINT ["/bin/sh", "/usr/local/bin/entrypoint.sh"] 20 | EXPOSE 8001 21 | CMD /bin/sh 22 | -------------------------------------------------------------------------------- /cubiti-server/cli.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let Promise = require('bluebird'); 4 | let util = require('./util.js'); 5 | 6 | let listeners = []; 7 | 8 | var CLI = function (app) { 9 | let db = app.db; 10 | let cmd = app.cmd; 11 | 12 | this.vorpal = require('vorpal')(); 13 | 14 | this.vorpal.command('event send ', 'send an event').action((args, done) => { 15 | app.ev.send(args.type, args.userid, args.object, args.actor).then(() => { 16 | done(); 17 | }); 18 | }); 19 | 20 | this.vorpal.command('event listen ', 'monitor events').action((args, done) => { 21 | app.ev.listen().then((listener) => { 22 | listeners.push({userid: args.userid, listener: listener}); 23 | listener.ondata(args.userid, (msg) => { 24 | console.log("event received", args.userid, msg); 25 | }); 26 | done(); 27 | }); 28 | }); 29 | 30 | this.vorpal.command('event listenstop ', 'stop monitoring events').action((args, done) => { 31 | listeners.forEach((l) => { 32 | if (l.userid == args.userid) { 33 | l.listener.close(); 34 | } 35 | }); 36 | done(); 37 | }); 38 | 39 | this.vorpal.command('db ls', 'list db tables').action((args, done) => { 40 | db.table_list().then((tables) => { 41 | console.log(tables); 42 | done(); 43 | }); 44 | }); 45 | 46 | this.vorpal.command('user ls', 'list users').action((args, done) => { 47 | db.user_all().then((users) => { 48 | console.log(users); 49 | done(); 50 | }); 51 | }); 52 | 53 | this.vorpal.command('user get ', 'get user').action((args, done) => { 54 | db.user_get(args.userid).then((user) => { 55 | console.log(user); 56 | done(); 57 | }).catch((err) => { 58 | console.log(err); 59 | done(); 60 | }); 61 | }); 62 | 63 | this.vorpal.command('user add ', 'add user').action((args, done) => { 64 | db.user_add({username: args.username, password: args.password}).then((user) => { 65 | console.log(user); 66 | done(); 67 | }).catch((err) => { 68 | console.log(err); 69 | done(); 70 | }); 71 | }); 72 | 73 | this.vorpal.command('user del ', 'del user').action((args, done) => { 74 | db.user_del({userid: args.userid}).then(() => { 75 | done(); 76 | }).catch((err) => { 77 | console.log(err); 78 | done(); 79 | }); 80 | }); 81 | 82 | this.vorpal.command('user validatetoken ', 'validate user').action((args, done) => { 83 | db.user_validateToken(args.token).then((user) => { 84 | console.log(user); 85 | done(); 86 | }).catch((err) => { 87 | console.log(err); 88 | done(); 89 | }); 90 | }); 91 | 92 | this.vorpal.command('user validatepw ', 'validate user').action((args, done) => { 93 | db.user_validatePassword({username: args.username, password: args.password}).then((user) => { 94 | console.log(user); 95 | done(); 96 | }).catch((err) => { 97 | console.log(err); 98 | done(); 99 | }); 100 | }); 101 | 102 | this.vorpal.command('token ls', 'list tokens').action((args, done) => { 103 | db.token_all().then((tokens) => { 104 | console.log(tokens); 105 | done(); 106 | }); 107 | }); 108 | 109 | this.vorpal.command('token add ', 'add token').action((args, done) => { 110 | db.token_add({userid: args.userid}, args.scope, false).then((tokenrecord) => { 111 | console.log(tokenrecord); 112 | done(); 113 | }).catch((err) => { 114 | console.log(err); 115 | done(); 116 | }); 117 | }); 118 | 119 | this.vorpal.command('token del ', 'del token').action((args, done) => { 120 | db.token_del(args.token).then(() => { 121 | done(); 122 | }).catch((err) => { 123 | console.log(err); 124 | done(); 125 | }); 126 | }); 127 | 128 | this.vorpal.command('token delstale', 'del stale tokens').action((args, done) => { 129 | db.token_delStale().then(() => { 130 | done(); 131 | }).catch((err) => { 132 | console.log(err); 133 | done(); 134 | }); 135 | }); 136 | 137 | this.vorpal.command('follower add ', 'add follower').action((args, done) => { 138 | db.follower_add(args.userid, args.actor).then(() => { 139 | done(); 140 | }).catch((err) => { 141 | console.log(err); 142 | done(); 143 | }); 144 | }); 145 | 146 | this.vorpal.command('follower del ', 'del follower').action((args, done) => { 147 | db.follower_del(args.userid, args.actor).then(() => { 148 | done(); 149 | }).catch((err) => { 150 | console.log(err); 151 | done(); 152 | }); 153 | }); 154 | 155 | this.vorpal.command('follower delall ', 'del all followers for user').action((args, done) => { 156 | db.follower_delAllForUser(args.userid).then(() => { 157 | done(); 158 | }).catch((err) => { 159 | console.log(err); 160 | done(); 161 | }); 162 | }); 163 | 164 | this.vorpal.command('follower ls ', 'get all followers for user').action((args, done) => { 165 | db.follower_allForUser(args.userid).then((followers) => { 166 | console.log(followers); 167 | done(); 168 | }).catch((err) => { 169 | console.log(err); 170 | done(); 171 | }); 172 | }); 173 | 174 | this.vorpal.command('following add ', 'add following').action((args, done) => { 175 | db.following_add(args.userid, args.actor).then(() => { 176 | done(); 177 | }).catch((err) => { 178 | console.log(err); 179 | done(); 180 | }); 181 | }); 182 | 183 | this.vorpal.command('following del ', 'del following').action((args, done) => { 184 | db.following_del(args.userid, args.actor).then(() => { 185 | done(); 186 | }).catch((err) => { 187 | console.log(err); 188 | done(); 189 | }); 190 | }); 191 | 192 | this.vorpal.command('following delall ', 'del all following for user').action((args, done) => { 193 | db.following_delAllForUser(args.userid).then(() => { 194 | done(); 195 | }).catch((err) => { 196 | console.log(err); 197 | done(); 198 | }); 199 | }); 200 | 201 | this.vorpal.command('following ls ', 'get all following for user').action((args, done) => { 202 | db.following_allForUser(args.userid).then((following) => { 203 | console.log(following); 204 | done(); 205 | }).catch((err) => { 206 | console.log(err); 207 | done(); 208 | }); 209 | }); 210 | 211 | this.vorpal.command('actor add ', 'add actor json (directly)').action((args, done) => { 212 | db.actor_add(args.actor, args.json).then(() => { 213 | done(); 214 | }).catch((err) => { 215 | console.log(err); 216 | done(); 217 | }); 218 | }); 219 | 220 | this.vorpal.command('actor get ', 'get actor').action((args, done) => { 221 | db.actor_get(args.actor).then((actorData) => { 222 | console.log(actorData); 223 | done(); 224 | }).catch((err) => { 225 | console.log(err); 226 | done(); 227 | }); 228 | }); 229 | 230 | this.vorpal.command('actor delall', 'del all actors').action((args, done) => { 231 | db.actor_delAll().then(() => { 232 | done(); 233 | }).catch((err) => { 234 | console.log(err); 235 | done(); 236 | }); 237 | }); 238 | 239 | this.vorpal.command('actor ls', 'get all actors (ids only)').action((args, done) => { 240 | db.actor_getList().then((actorList) => { 241 | console.log(actorList); 242 | done(); 243 | }).catch((err) => { 244 | console.log(err); 245 | done(); 246 | }); 247 | }); 248 | 249 | this.vorpal.command('message add ', 'add message json (directly)').action((args, done) => { 250 | db.message_add(args.guid, args.message, args.actor).then(() => { 251 | done(); 252 | }).catch((err) => { 253 | console.log(err); 254 | done(); 255 | }); 256 | }); 257 | 258 | this.vorpal.command('message get ', 'get message').action((args, done) => { 259 | db.message_get(args.guid).then((messageData) => { 260 | console.log(JSON.stringify(messageData, null, 4)); 261 | done(); 262 | }).catch((err) => { 263 | console.log(err); 264 | done(); 265 | }); 266 | }); 267 | 268 | this.vorpal.command('message del ', 'del messages').action((args, done) => { 269 | db.message_del(args.guid).then(() => { 270 | done(); 271 | }).catch((err) => { 272 | console.log(err); 273 | done(); 274 | }); 275 | }); 276 | 277 | this.vorpal.command('message delall', 'del all messages').action((args, done) => { 278 | db.message_delAll().then(() => { 279 | done(); 280 | }).catch((err) => { 281 | console.log(err); 282 | done(); 283 | }); 284 | }); 285 | 286 | this.vorpal.command('message ls', 'get all messages (guids only)').action((args, done) => { 287 | db.message_getList().then((messageList) => { 288 | console.log(messageList); 289 | done(); 290 | }).catch((err) => { 291 | console.log(err); 292 | done(); 293 | }); 294 | }); 295 | 296 | this.vorpal.command('action sendnote ', 'send note to all followers').action((args, done) => { 297 | cmd.sendNote(app, args.userid, args.note).then(() => { 298 | done(); 299 | }).catch((err) => { 300 | console.log(err); 301 | done(); 302 | }); 303 | }); 304 | 305 | this.vorpal.command('action deletenote ', 'delete note to all followers').action((args, done) => { 306 | cmd.deleteNote(app, args.userid, args.note).then(() => { 307 | done(); 308 | }).catch((err) => { 309 | console.log(err); 310 | done(); 311 | }); 312 | }); 313 | 314 | this.vorpal.command('action follow ', 'send follow req to actor').action((args, done) => { 315 | cmd.sendFollow(app, args.userid, args.actor).then(() => { 316 | done(); 317 | }).catch((err) => { 318 | console.log(err); 319 | done(); 320 | }); 321 | }); 322 | 323 | this.vorpal.command('action unfollow ', 'send unfollow req to actor').action((args, done) => { 324 | cmd.sendUnfollow(app, args.userid, args.actor).then(() => { 325 | done(); 326 | }).catch((err) => { 327 | console.log(err); 328 | done(); 329 | }); 330 | }); 331 | 332 | this.vorpal.command('action like ', 'send like req to actor').action((args, done) => { 333 | cmd.sendLike(app, args.userid, args.objectid, args.actor).then(() => { 334 | done(); 335 | }).catch((err) => { 336 | console.log(err); 337 | done(); 338 | }); 339 | }); 340 | 341 | this.vorpal.command('action unlike ', 'send unlike req to actor').action((args, done) => { 342 | cmd.sendUnlike(app, args.userid, args.objectid, args.actor).then(() => { 343 | done(); 344 | }).catch((err) => { 345 | console.log(err); 346 | done(); 347 | }); 348 | }); 349 | 350 | 351 | this.vorpal.command('like add ', 'add like').action((args, done) => { 352 | db.like_add(args.guid, args.actor).then(() => { 353 | done(); 354 | }).catch((err) => { 355 | console.log(err); 356 | done(); 357 | }); 358 | }); 359 | 360 | this.vorpal.command('like ls', 'get all likes').action((args, done) => { 361 | db.like_getList().then((likeList) => { 362 | console.log(likeList); 363 | done(); 364 | }).catch((err) => { 365 | console.log(err); 366 | done(); 367 | }); 368 | }); 369 | 370 | this.vorpal.command('like del ', 'del like').action((args, done) => { 371 | db.like_del(args.guid, args.actor).then(() => { 372 | done(); 373 | }).catch((err) => { 374 | console.log(err); 375 | done(); 376 | }); 377 | }); 378 | 379 | this.vorpal.command('announce add ', 'add announce').action((args, done) => { 380 | db.announce_add(args.guid, args.actor).then(() => { 381 | done(); 382 | }).catch((err) => { 383 | console.log(err); 384 | done(); 385 | }); 386 | }); 387 | 388 | this.vorpal.command('announce ls', 'get all announces').action((args, done) => { 389 | db.announce_getList().then((announceList) => { 390 | console.log(announceList); 391 | done(); 392 | }).catch((err) => { 393 | console.log(err); 394 | done(); 395 | }); 396 | }); 397 | 398 | this.vorpal.command('announce del ', 'del announce').action((args, done) => { 399 | db.announce_del(args.guid, args.actor).then(() => { 400 | done(); 401 | }).catch((err) => { 402 | console.log(err); 403 | done(); 404 | }); 405 | }); 406 | 407 | this.vorpal.command('announce delall', 'delall announce').action((args, done) => { 408 | db.announce_delAll().then(() => { 409 | done(); 410 | }).catch((err) => { 411 | console.log(err); 412 | done(); 413 | }); 414 | }); 415 | 416 | this.vorpal.command('media del ', 'del medias').action((args, done) => { 417 | db.media_del(args.guid).then(() => { 418 | done(); 419 | }).catch((err) => { 420 | console.log(err); 421 | done(); 422 | }); 423 | }); 424 | 425 | this.vorpal.command('media delall', 'del all medias').action((args, done) => { 426 | db.media_delAll().then(() => { 427 | done(); 428 | }).catch((err) => { 429 | console.log(err); 430 | done(); 431 | }); 432 | }); 433 | 434 | this.vorpal.command('media add ', 'add media').action((args, done) => { 435 | db.media_add(args.guid, args.userid, args.file, args.preview, args.type, args.blurhash, args.description, args.meta).then(() => { 436 | done(); 437 | }).catch((err) => { 438 | console.log(err); 439 | done(); 440 | }); 441 | }); 442 | 443 | this.vorpal.command('media get ', 'get media').action((args, done) => { 444 | db.media_get(args.guid).then((media) => { 445 | console.log(media); 446 | done(); 447 | }).catch((err) => { 448 | console.log(err); 449 | done(); 450 | }); 451 | }); 452 | 453 | this.vorpal.command('media ls', 'get all medias').action((args, done) => { 454 | db.media_getList().then((mediaList) => { 455 | console.log(mediaList); 456 | done(); 457 | }).catch((err) => { 458 | console.log(err); 459 | done(); 460 | }); 461 | }); 462 | 463 | this.vorpal.command('timeline ', 'timeline').action((args, done) => { 464 | db.user_get(args.userid).then((user) => { 465 | db.timeline(args.userid, util.userActor(app, user)).then((messages) => { 466 | //console.log(messages); 467 | messages.forEach((m) => { 468 | console.log(m.guid, m.actor, m.created_at, m.message.type, m.message.object.content); 469 | }); 470 | done(); 471 | }).catch((err) => { 472 | console.log(err); 473 | done(); 474 | }); 475 | }); 476 | 477 | }); 478 | 479 | }; 480 | 481 | CLI.prototype.run = async function () { 482 | this.vorpal.delimiter('>').show(); 483 | } 484 | 485 | module.exports = CLI; 486 | -------------------------------------------------------------------------------- /cubiti-server/cmd.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let Promise = require('bluebird'); 4 | const crypto = require('crypto'); 5 | var urlparser = require('url'); 6 | var util = require('./util.js'); 7 | var fetch = require('node-fetch'); 8 | 9 | var CMD = function (db) { 10 | this.db = db; 11 | }; 12 | 13 | CMD.prototype.getActor = async function (app, user, actor) { 14 | let rsp = null; 15 | 16 | // local actor 17 | if (actor.startsWith(`https://${app.config.server.domain}`)) { 18 | return Promise.resolve(util.createActor(user.username, app.config.server.domain, user.pubkey)); 19 | } 20 | 21 | return app.db.actor_get(actor).then((actorData) => { 22 | if (actorData === null) { 23 | console.log(`getActor: cache miss for ${actor} fetching...`); 24 | // fetch and store 25 | return fetch(actor, { 26 | method: 'GET', 27 | headers: { 28 | 'Accept': 'application/activity+json' 29 | } 30 | }).then((res) => { 31 | return res.json(); 32 | }).then((json) => { 33 | rsp = json; 34 | console.log(`getActor: cache miss for ${actor} storing...`); 35 | return app.db.actor_add(actor, rsp); 36 | }).then(() => { 37 | return Promise.resolve(rsp); 38 | }); 39 | } else { 40 | // console.log(`getActor: cache hit for ${actor}`); 41 | return Promise.resolve(actorData.json); 42 | } 43 | }); 44 | } 45 | 46 | CMD.prototype.signObjectAndSend = function signObjectAndSend(app, user, msg, actor, allow_failure) { 47 | let cmd = this; 48 | let status; 49 | if (!user.privkey) { 50 | return Promise.reject('signObjectAndSend user has no privkey'); 51 | } 52 | return cmd.getActor(app, user, actor).then((actorAccount) => { 53 | let inbox = actorAccount.inbox; 54 | let httpStatus; 55 | if (!inbox) { 56 | return Promise.reject('bad inbox'); 57 | } 58 | const inboxUrlComponents = urlparser.parse(inbox); 59 | const digestHash = crypto.createHash('sha256').update(JSON.stringify(msg)).digest('base64'); 60 | const signer = crypto.createSign('sha256'); 61 | let d = new Date(); 62 | let stringToSign = `(request-target): post ${inboxUrlComponents.path}\nhost: ${inboxUrlComponents.host}\ndate: ${d.toUTCString()}\ndigest: SHA-256=${digestHash}`; 63 | signer.update(stringToSign); 64 | signer.end(); 65 | const signature = signer.sign(user.privkey); 66 | const signature_b64 = signature.toString('base64'); 67 | let header = `keyId="https://${app.config.server.domain}/u/${user.username}",headers="(request-target) host date digest",signature="${signature_b64}"`; 68 | 69 | try { 70 | return fetch(inbox, { 71 | method: 'POST', 72 | body: JSON.stringify(msg), 73 | headers: { 74 | 'Host': inboxUrlComponents.host, 75 | 'Date': d.toUTCString(), 76 | 'Digest': `SHA-256=${digestHash}`, 77 | 'Signature': header 78 | } 79 | }).then((res) => { 80 | httpStatus = res.status; 81 | if (res.status >= 200 && res.status < 300) { 82 | console.log(`post to inbox ok ${httpStatus}`); 83 | return Promise.resolve(); 84 | } else { 85 | console.log(`post to inbox failed ${httpStatus}`); 86 | if (allow_failure) { 87 | console.log(`post to inbox failed ${status}, continuing...`); 88 | return Promise.resolve(); 89 | } else { 90 | return Promise.reject(`post to inbox failed ${status}`); 91 | } 92 | } 93 | }).catch((err) => { 94 | console.log("Caught fetch error: ", err); 95 | if (allow_failure) { 96 | return Promise.resolve(); 97 | } else { 98 | return Promise.reject(`post to inbox failed ${status} err`); 99 | } 100 | }); 101 | } catch (e) { 102 | if (allow_failure) { 103 | console.log(`post to inbox failed ${e}, continuing...`); 104 | return Promise.resolve(); 105 | } else { 106 | return Promise.reject(`post to inbox failed ${e}`); 107 | } 108 | } 109 | }); 110 | } 111 | 112 | function createNoteMsg(app, user, text, guid, in_reply_to_id, attachment, spoiler_text, sensitive) { 113 | let msg; 114 | msg = { 115 | 'id': `${util.getMsgPrefix(app)}${guid}/object`, 116 | 'type': 'Note', 117 | 'published': (new Date()).toISOString(), 118 | 'attributedTo': util.userActor(app, user), 119 | 'content': text, 120 | 'inReplyTo': in_reply_to_id || null, 121 | 'to': ['https://www.w3.org/ns/activitystreams#Public'], 122 | }; 123 | if (attachment && attachment.length > 0) { 124 | msg.attachment = attachment; 125 | } 126 | if (spoiler_text) { 127 | msg.summary = spoiler_text; 128 | } 129 | if (sensitive) { 130 | msg.sensitive = sensitive; 131 | } 132 | return msg; 133 | } 134 | 135 | 136 | function createCreateMsg(app, user, text, in_reply_to_id, attachment, spoiler_text, sensitive) { 137 | let msg; 138 | return util.generateGuid().then((guid) => { 139 | msg = { 140 | '@context': 'https://www.w3.org/ns/activitystreams', 141 | 'id': `${util.getMsgPrefix(app)}${guid}`, 142 | 'type': 'Create', 143 | 'actor': util.userActor(app, user), 144 | 'published': (new Date()).toISOString(), 145 | 'to': ['https://www.w3.org/ns/activitystreams#Public'], 146 | 'cc': [util.userActor(app, user) + '/followers'], 147 | 'object': createNoteMsg(app, user, text, guid, in_reply_to_id, attachment, spoiler_text, sensitive) 148 | }; 149 | //console.log("createCreateMsg", msg, `${util.getMsgPrefix(app)}${guid}`); 150 | // save message, single generic message in db 151 | return app.db.message_add(`${util.getMsgPrefix(app)}${guid}`, msg, util.userActor(app, user), msg.object.id).then(() => { 152 | return Promise.resolve(msg); 153 | }); 154 | }); 155 | } 156 | 157 | CMD.prototype.sendNote = async function (app, userid, text, in_reply_to_id, attachment, spoiler_text, sensitive) { 158 | let cmd = this; 159 | let user; 160 | console.log(`CMD.sendNote ${userid} '${text}' ${in_reply_to_id} ${attachment} ${spoiler_text} ${sensitive}`); 161 | let cm; 162 | 163 | // get user 164 | return app.db.user_getByIdPrivKey(userid).then((_user) => { 165 | user = _user; 166 | // create a generic Create+Note cc'd to actor+'/followers', saved in db 167 | return createCreateMsg(app, user, text, in_reply_to_id, attachment, spoiler_text, sensitive).then((createMsg) => { 168 | cm = createMsg; 169 | return Promise.resolve(cm); 170 | }).then((createMsg) => { 171 | // deliver to each follower 172 | return app.db.follower_allForUser(userid).then((followers) => { 173 | // allow some to fail, FIXME, need a systemwide mechanism for retries 174 | return Promise.each(followers, function(follower, index, arrayLength) { 175 | // send to follower 176 | createMsg.object.cc = [follower.actor]; // restamp destination actor 177 | console.log("Sending to ", follower.actor/*, createMsg*/); 178 | return app.cmd.signObjectAndSend(app, user, createMsg, follower.actor, true); 179 | }); 180 | }); 181 | }); 182 | }).then(() => { 183 | return Promise.resolve(cm); // send back the msg 184 | }); 185 | } 186 | 187 | function createDeleteMsg(app, user, actor, objectid) { 188 | let msg; 189 | return util.generateGuid().then((guid) => { 190 | msg = { 191 | '@context': 'https://www.w3.org/ns/activitystreams', 192 | 'id': `${util.getMsgPrefix(app)}${guid}`, 193 | 'type': 'Delete', 194 | 'actor': util.userActor(app, user), 195 | 'to': ['https://www.w3.org/ns/activitystreams#Public'], 196 | 'cc': [actor], 197 | 'object': objectid 198 | }; 199 | //console.log("createDeleteMsg", msg, `${util.getMsgPrefix(app)}${guid}`); 200 | return Promise.resolve(msg); 201 | }); 202 | } 203 | 204 | CMD.prototype.deleteNoteToActor = async function (app, user, actor, objectid) { 205 | let cmd = this; 206 | console.log(`CMD.deleteNoteToActor ${user.username} ${actor} ${objectid}`); 207 | return createDeleteMsg(app, user, actor, objectid).then((deleteMsg) => { 208 | return app.cmd.signObjectAndSend(app, user, deleteMsg, actor, true); 209 | }); 210 | } 211 | 212 | CMD.prototype.deleteNote = async function (app, userid, objectid) { 213 | let cmd = this; 214 | console.log(`CMD.deleteNote ${userid} ${objectid}`); 215 | 216 | // get user 217 | return app.db.user_getByIdPrivKey(userid).then((user) => { 218 | return app.db.follower_allForUser(userid).then((followers) => { 219 | // allow some to fail, FIXME, need a systemwide mechanism for retries 220 | return Promise.each(followers, function(follower, index, arrayLength) { 221 | // delete to follower 222 | return cmd.deleteNoteToActor(app, user, follower.actor, objectid); 223 | }); 224 | }).then(() => { 225 | // delete message from db, FIXME this will only happen if every deletion req succeeded 226 | // should allow to fail, retry etc. 227 | return app.db.message_delByObjectId(objectid); 228 | }); 229 | }); 230 | } 231 | 232 | function createFollowMsg(app, user, followActor) { 233 | let msg; 234 | return util.generateGuid().then((guid) => { 235 | let d = new Date(); 236 | msg = { 237 | '@context': 'https://www.w3.org/ns/activitystreams', 238 | 'id': `${util.getMsgPrefix(app)}${guid}`, 239 | 'type': 'Follow', 240 | 'actor': util.userActor(app, user), 241 | 'object': followActor 242 | }; 243 | return Promise.resolve(msg); 244 | }); 245 | } 246 | 247 | CMD.prototype.sendFollowToActor = async function (app, user, targetActor) { 248 | let cmd = this; 249 | return createFollowMsg(app, user, targetActor).then((followMsg) => { 250 | console.log(followMsg); 251 | return app.cmd.signObjectAndSend(app, user, followMsg, targetActor, false); 252 | }); 253 | } 254 | 255 | CMD.prototype.sendFollow = async function (app, userid, targetActor) { 256 | let cmd = this; 257 | console.log(`CMD.sendFollow ${userid} '${targetActor}'`); 258 | 259 | // get user 260 | return app.db.user_getByIdPrivKey(userid).then((user) => { 261 | return cmd.sendFollowToActor(app, user, targetActor); 262 | }).then(() => { 263 | return app.db.following_add(userid, targetActor); 264 | }); 265 | } 266 | 267 | function createUndoMsg(app, user, object) { 268 | let msg; 269 | return util.generateGuid().then((guid) => { 270 | let d = new Date(); 271 | msg = { 272 | '@context': 'https://www.w3.org/ns/activitystreams', 273 | 'id': `${util.getMsgPrefix(app)}${guid}`, 274 | 'type': 'Undo', 275 | 'to': [ 'https://www.w3.org/ns/activitystreams#Public' ], 276 | 'actor': util.userActor(app, user), 277 | 'object': object 278 | }; 279 | return Promise.resolve(msg); 280 | }); 281 | } 282 | 283 | CMD.prototype.sendUnfollowToActor = async function (app, user, targetActor) { 284 | let cmd = this; 285 | return createFollowMsg(app, user, targetActor).then((followMsg) => { 286 | //console.log(followMsg); 287 | return createUndoMsg(app, user, followMsg); 288 | }).then((undoMsg) => { 289 | return app.cmd.signObjectAndSend(app, user, undoMsg, targetActor, false); 290 | }); 291 | } 292 | 293 | CMD.prototype.sendUnfollow = async function (app, userid, targetActor) { 294 | let cmd = this; 295 | console.log(`CMD.sendUnfollow ${userid} '${targetActor}'`); 296 | 297 | // get user 298 | return app.db.user_getByIdPrivKey(userid).then((user) => { 299 | return cmd.sendUnfollowToActor(app, user, targetActor); 300 | }).then(() => { 301 | return app.db.following_del(userid, targetActor); 302 | }); 303 | } 304 | 305 | function createLikeMsg(app, user, objectid) { 306 | let msg; 307 | return util.generateGuid().then((guid) => { 308 | let d = new Date(); 309 | msg = { 310 | '@context': 'https://www.w3.org/ns/activitystreams', 311 | 'id': `${util.getMsgPrefix(app)}${guid}`, 312 | 'type': 'Like', 313 | 'actor': util.userActor(app, user), 314 | 'object': objectid 315 | }; 316 | console.log(msg); 317 | return Promise.resolve(msg); 318 | }); 319 | } 320 | 321 | CMD.prototype.sendLikeToActor = async function (app, user, objectid, targetActor) { 322 | let cmd = this; 323 | return createLikeMsg(app, user, objectid).then((likeMsg) => { 324 | console.log(likeMsg); 325 | return app.cmd.signObjectAndSend(app, user, likeMsg, targetActor, false); 326 | }); 327 | } 328 | 329 | CMD.prototype.sendLike = function (app, userid, objectid, targetActor) { 330 | let cmd = this; 331 | let user; 332 | console.log(`CMD.sendLike ${userid} '${targetActor}'`); 333 | 334 | // get user 335 | return app.db.user_getByIdPrivKey(userid).then((_user) => { 336 | user = _user; 337 | return cmd.sendLikeToActor(app, user, objectid, targetActor); 338 | }).then(() => { 339 | return app.db.like_add(objectid, util.userActor(app, user)); 340 | }); 341 | } 342 | 343 | CMD.prototype.sendUnlikeToActor = async function (app, user, objectid, targetActor) { 344 | let cmd = this; 345 | return createLikeMsg(app, user, objectid, targetActor).then((likeMsg) => { 346 | return createUndoMsg(app, user, likeMsg); 347 | }).then((undoMsg) => { 348 | return app.cmd.signObjectAndSend(app, user, undoMsg, targetActor, false); 349 | }); 350 | } 351 | 352 | CMD.prototype.sendUnlike = function (app, userid, objectid, targetActor) { 353 | let cmd = this; 354 | let user; 355 | console.log(`CMD.sendUnlike ${userid} '${targetActor}'`); 356 | 357 | // get user 358 | return app.db.user_getByIdPrivKey(userid).then((_user) => { 359 | user = _user; 360 | return cmd.sendUnlikeToActor(app, user, objectid, targetActor); 361 | }).then(() => { 362 | return app.db.like_del(objectid, util.userActor(app, user)); 363 | }); 364 | } 365 | 366 | function createAnnounceMsg(app, user, objectid, targetActor, store) { 367 | let msg; 368 | let d = new Date(); 369 | 370 | return util.generateGuid().then((guid) => { 371 | //id has to match on announce and undo announce 372 | //Can either make it something predictable, or exract the original announce from the db 373 | // doesn't need to be resolveable, just an id we can regenerate for the undo announce 374 | guid = util.encodeSafeB64(`${objectid}/${targetActor}`); 375 | 376 | let d = new Date(); 377 | msg = { 378 | '@context': 'https://www.w3.org/ns/activitystreams', 379 | 'id': `${util.getMsgPrefix(app)}${guid}`, 380 | 'type': 'Announce', 381 | 'actor': util.userActor(app, user), 382 | 'published': d.toISOString(), 383 | 'to': [ 'https://www.w3.org/ns/activitystreams#Public' ], 384 | 'cc': [ targetActor, util.userActor(app, user) + '/followers' ], 385 | 'object': objectid 386 | }; 387 | 388 | // fill out object, so that db has whole record 389 | console.log("CAM get", objectid); 390 | return app.db.message_get(objectid).then((object) => { 391 | console.log("OBJ", object); 392 | // fill out msg.object with the Note 393 | msg.object = object.message.object; // fill out msg.object 394 | return Promise.resolve(); 395 | }).then(() => { 396 | if (store) { 397 | return app.db.message_add(`${util.getMsgPrefix(app)}${guid}`, msg, util.userActor(app, user), objectid); 398 | } else { 399 | return Promise.resolve(); 400 | } 401 | }).then(() => { 402 | msg.object = objectid; // go back to bare id 403 | return Promise.resolve(msg); 404 | }); 405 | }); 406 | } 407 | 408 | CMD.prototype.sendAnnounce = function (app, userid, objectid, targetActor) { 409 | let cmd = this; 410 | let user; 411 | let msg; 412 | console.log(`CMD.sendAnnounce ${userid} '${objectid} '${targetActor}'`); 413 | 414 | // get user 415 | return app.db.user_getByIdPrivKey(userid).then((_user) => { 416 | user = _user; 417 | return Promise.resolve(); 418 | }).then(() => { 419 | // construct message 420 | return createAnnounceMsg(app, user, objectid, targetActor, true); 421 | }).then((announceMsg) => { 422 | msg = announceMsg; 423 | return Promise.resolve(); 424 | }).then(() => { 425 | // record that user announced it 426 | return app.db.announce_add(objectid, util.userActor(app, user)); 427 | }).then(() => { 428 | // send to targetActor (original poster) 429 | return app.cmd.signObjectAndSend(app, user, msg, targetActor, false); 430 | }).then(() => { 431 | // cc announce to user's followers 432 | return app.db.follower_allForUser(userid).then((followers) => { 433 | return Promise.each(followers, function(follower, index, arrayLength) { 434 | // send to follower 435 | msg.cc = [follower.actor]; // restamp destination actor 436 | console.log("Sending announce to ", follower.actor); 437 | return app.cmd.signObjectAndSend(app, user, msg, follower.actor, true); 438 | }); 439 | }); 440 | }); 441 | } 442 | 443 | CMD.prototype.sendUnannounce = function (app, userid, objectid, targetActor) { 444 | let cmd = this; 445 | console.log(`CMD.sendUnannounce ${userid} '${objectid}' '${targetActor}'`); 446 | 447 | // get user 448 | return app.db.user_getByIdPrivKey(userid).then((_user) => { 449 | user = _user; 450 | return Promise.resolve(); 451 | }).then(() => { 452 | // construct message 453 | return createAnnounceMsg(app, user, objectid, targetActor, false).then((announceMsg) => { 454 | return createUndoMsg(app, user, announceMsg); 455 | }); 456 | 457 | }).then((undoMsg) => { 458 | msg = undoMsg; 459 | return Promise.resolve(); 460 | }).then(() => { 461 | // delete record that user announced it 462 | return app.db.announce_del(objectid, util.userActor(app, user)); 463 | }).then(() => { 464 | // reblog created two entries, the original and the announce+note, delete the clone 465 | return app.db.message_delByObjectIdAndActor(objectid, util.userActor(app, user)); 466 | }).then(() => { 467 | // send to targetActor (original poster) 468 | return app.cmd.signObjectAndSend(app, user, msg, targetActor, false); 469 | }).then(() => { 470 | // cc unannounce to user's followers 471 | return app.db.follower_allForUser(userid).then((followers) => { 472 | // allow some to fail, FIXME, need a systemwide mechanism for retries 473 | return Promise.each(followers, function(follower, index, arrayLength) { 474 | // send to follower 475 | delete msg.object['@context']; // is this necessary? 476 | console.log("Sending undo-announce to ", follower.actor); 477 | return app.cmd.signObjectAndSend(app, user, msg, follower.actor, true); 478 | }); 479 | }); 480 | }); 481 | } 482 | 483 | module.exports = CMD; 484 | -------------------------------------------------------------------------------- /cubiti-server/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require('fs'); 4 | 5 | var CONFIG = function(filename) { 6 | const data = fs.readFileSync(filename, {encoding:'utf8', flag:'r'}); 7 | const json = JSON.parse(data); 8 | console.log("Reading " + filename); 9 | console.log(json); 10 | 11 | Object.keys(json).forEach((key) => { 12 | this[key] = json[key]; 13 | }); 14 | }; 15 | 16 | module.exports = CONFIG; 17 | -------------------------------------------------------------------------------- /cubiti-server/db.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let Promise = require('bluebird'); 4 | const sqlite3 = require('sqlite3').verbose(); 5 | const bcrypt = require('bcrypt'); 6 | const saltRounds = 10; 7 | const crypto = require('crypto'); 8 | const debug = false; 9 | const TOKENLEN = 48; 10 | 11 | // table definitions 12 | let sqlTableDefs = [ 13 | 'CREATE TABLE IF NOT EXISTS users (userid INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, pwhash TEXT NOT NULL, pubkey TEXT NOT NULL, privkey TEXT NOT NULL)', 14 | // if code == null, token has been fetched via oauth and is live 15 | 'CREATE TABLE IF NOT EXISTS tokens (token TEXT NOT NULL PRIMARY KEY, code TEXT, created_at INTEGER NOT NULL, userid INTEGER NOT NULL, scope TEXT NOT NULL, deleteable INTEGER NOT NULL)', 16 | 'CREATE TABLE IF NOT EXISTS followers (userid INTEGER, actor TEXT NOT NULL, followed_at INTEGER NOT NULL, UNIQUE(userid, actor))', 17 | 'CREATE TABLE IF NOT EXISTS following (userid INTEGER, actor TEXT NOT NULL, followed_at INTEGER NOT NULL, UNIQUE(userid, actor))', 18 | 'CREATE TABLE IF NOT EXISTS actors (actor TEXT NOT NULL PRIMARY KEY, json TEXT NOT NULL, created_at INTEGER NOT NULL)', 19 | 'CREATE TABLE IF NOT EXISTS messages (guid TEXT NOT NULL PRIMARY KEY, message TEXT NOT NULL, actor TEXT NOT NULL, objectid TEXT, created_at INTEGER NOT NULL, published_at INTEGER NOT NULL)', 20 | 'CREATE TABLE IF NOT EXISTS likes (guid TEXT NOT NULL PRIMARY KEY, actor TEXT NOT NULL, created_at INTEGER NOT NULL)', 21 | 'CREATE TABLE IF NOT EXISTS announces (guid TEXT NOT NULL PRIMARY KEY, actor TEXT NOT NULL, created_at INTEGER NOT NULL)', 22 | 'CREATE TABLE IF NOT EXISTS medias (guid TEXT NOT NULL PRIMARY KEY, userid integer NOT NULL, created_at INTEGER NOT NULL, file BLOB NOT NULL, preview BLOB NOT NULL, type TEXT NOT NULL, blurhash TEXT NOT NULL, description TEXT NOT NULL, meta TEXT NOT NULL)' 23 | ]; 24 | 25 | var DB = function () { 26 | this.db = null; 27 | }; 28 | 29 | DB.prototype.runSql = async function (sqlStmt) { 30 | return new Promise((resolve, reject) => { 31 | this.db.serialize(() => { 32 | this.db.run(sqlStmt, [], (err) => { 33 | if (err) { 34 | reject(err); 35 | } else { 36 | resolve(); 37 | } 38 | }); 39 | }); 40 | }); 41 | } 42 | 43 | DB.prototype.open = async function (filename) { 44 | let self = this; 45 | return new Promise((resolve, reject) => { 46 | this.db = new sqlite3.Database(filename, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE | sqlite3.OPEN_FULLMUTEX, (err) => { 47 | if (err) { 48 | reject("db open err"); 49 | } else { 50 | resolve(); 51 | } 52 | }); 53 | }).then(() => { 54 | if (debug) { 55 | this.db.on('trace', (q) => { 56 | console.log(q); 57 | }); 58 | this.db.on('profile', (q, t) => { 59 | console.log(q + ' [' + t + 'ms]'); 60 | }); 61 | } 62 | 63 | return Promise.each(sqlTableDefs, function(tblDef, index, arrayLength) { 64 | return self.runSql(tblDef); 65 | }); 66 | }); 67 | } 68 | 69 | 70 | DB.prototype.close = async function () { 71 | return new Promise((resolve, reject) => { 72 | this.db.close((err) => { 73 | if (err) { 74 | reject(err); 75 | } else { 76 | resolve(); 77 | } 78 | }); 79 | }); 80 | } 81 | 82 | // user.username, user.password 83 | DB.prototype.user_validatePassword = async function (user) { 84 | return new Promise((resolve, reject) => { 85 | this.db.serialize(() => { 86 | let retuser = null; 87 | this.db.each("SELECT userid, username, pwhash from users WHERE username == ?", [user.username], (err, row) => { 88 | retuser = { 89 | userid: row.userid, 90 | username: row.username, 91 | pwhash: row.pwhash 92 | }; 93 | }, (err) => { 94 | if (err) { 95 | reject(err); 96 | } else { 97 | resolve(retuser); 98 | } 99 | }); 100 | }); 101 | }).then((userRecord) => { 102 | if (userRecord === null) { 103 | return Promise.resolve(null); 104 | } else { 105 | return new Promise((resolve, reject) => { 106 | bcrypt.compare(user.password, userRecord.pwhash, (err, result) => { 107 | if (err) { 108 | console.log(err); 109 | resolve(null); 110 | } else { 111 | if (!result) { // mismatch 112 | resolve(null); 113 | } else { 114 | delete userRecord.pwhash; 115 | resolve(userRecord); 116 | } 117 | } 118 | }); 119 | }); 120 | } 121 | }); 122 | } 123 | 124 | DB.prototype.user_validateToken = async function (token) { 125 | return new Promise((resolve, reject) => { 126 | this.db.serialize(() => { 127 | let userid = null; 128 | this.db.each("SELECT userid, token FROM tokens WHERE token == ?", [token], (err, row) => { 129 | if (err) { 130 | resolve(err); 131 | } else { 132 | userid = row.userid; 133 | } 134 | }, (err) => { 135 | if (err) { 136 | reject(err); 137 | } else { 138 | resolve(userid); 139 | } 140 | }); 141 | }); 142 | }).then((userid) => { 143 | if (userid === null) { 144 | return Promise.resolve(null); 145 | } else { 146 | return this.user_get(userid); 147 | } 148 | }); 149 | } 150 | 151 | DB.prototype.user_add = async function (user) { 152 | 153 | return new Promise((resolve, reject) => { 154 | let db = this.db; 155 | 156 | const rsaOptions = { 157 | modulusLength: 4096, 158 | publicKeyEncoding: { 159 | type: 'spki', 160 | format: 'pem' 161 | }, 162 | privateKeyEncoding: { 163 | type: 'pkcs8', 164 | format: 'pem' 165 | } 166 | }; 167 | 168 | crypto.generateKeyPair('rsa', rsaOptions, (err, publicKey, privateKey) => { 169 | if (err) { 170 | reject(err); 171 | } else { 172 | bcrypt.hash(user.password, saltRounds, function(err, pwhash) { 173 | db.serialize(() => { 174 | db.run("INSERT INTO users (username, pwhash, pubkey, privkey) VALUES(?,?,?,?)", [user.username, pwhash, publicKey, privateKey], (err) => { 175 | if (err) { 176 | reject(err); 177 | } else { 178 | } 179 | }, (err) => { 180 | if (err) { 181 | reject(err); 182 | } else { 183 | let retuser = null; 184 | db.each("SELECT userid, username, pubkey from users WHERE username == ?", [user.username], (err, row) => { 185 | retuser = { 186 | userid: row.userid, 187 | username: row.username, 188 | pubkey: row.pubkey 189 | }; 190 | }, (err) => { 191 | if (err) { 192 | reject(err); 193 | } else { 194 | resolve(retuser); 195 | } 196 | }); 197 | } 198 | }); 199 | }); 200 | }); 201 | } 202 | }); 203 | }).then((retuser) => { 204 | return this.token_add(retuser, '*', true).then(() => { 205 | return Promise.resolve(retuser); 206 | }); 207 | }); 208 | } 209 | 210 | DB.prototype.user_del = async function (user) { 211 | return new Promise((resolve, reject) => { 212 | this.db.serialize(() => { 213 | this.db.run("DELETE FROM users WHERE userid == (?)", [user.userid], (err) => { 214 | if (err) { 215 | reject(err); 216 | } else { 217 | } 218 | }, (err) => { 219 | if (err) { 220 | reject(err); 221 | } else { 222 | resolve(); 223 | } 224 | }); 225 | }); 226 | }).then(() => { 227 | this.token_delByUser(user); 228 | }).then(() => { 229 | this.follower_delAllForUser(user.userid); 230 | }).then(() => { 231 | this.following_delAllForUser(user.userid); 232 | }); 233 | } 234 | 235 | DB.prototype.user_getByName = async function (username) { 236 | let user = null; 237 | return new Promise((resolve, reject) => { 238 | this.db.serialize(() => { 239 | this.db.each("SELECT userid, username, pubkey from users WHERE username = (?)", [username], (err, row) => { 240 | if (err) { 241 | reject(err); 242 | } else { 243 | user = { 244 | userid: row.userid, 245 | username: row.username, 246 | pubkey: row.pubkey 247 | }; 248 | } 249 | }, (err) => { 250 | if (err) { 251 | reject(err); 252 | } else { 253 | resolve(user); 254 | } 255 | }); 256 | }); 257 | }); 258 | } 259 | 260 | DB.prototype.user_getByNamePrivKey = async function (username) { 261 | let user = null; 262 | return new Promise((resolve, reject) => { 263 | this.db.serialize(() => { 264 | this.db.each("SELECT userid, username, pubkey, privkey from users WHERE username = (?)", [username], (err, row) => { 265 | if (err) { 266 | reject(err); 267 | } else { 268 | user = { 269 | userid: row.userid, 270 | username: row.username, 271 | pubkey: row.pubkey, 272 | privkey: row.privkey 273 | }; 274 | } 275 | }, (err) => { 276 | if (err) { 277 | reject(err); 278 | } else { 279 | resolve(user); 280 | } 281 | }); 282 | }); 283 | }); 284 | } 285 | 286 | DB.prototype.user_getByIdPrivKey = async function (userid) { 287 | let user = null; 288 | return new Promise((resolve, reject) => { 289 | this.db.serialize(() => { 290 | this.db.each("SELECT userid, username, pubkey, privkey from users WHERE userid = (?)", [userid], (err, row) => { 291 | if (err) { 292 | reject(err); 293 | } else { 294 | user = { 295 | userid: row.userid, 296 | username: row.username, 297 | pubkey: row.pubkey, 298 | privkey: row.privkey 299 | }; 300 | } 301 | }, (err) => { 302 | if (err) { 303 | reject(err); 304 | } else { 305 | resolve(user); 306 | } 307 | }); 308 | }); 309 | }); 310 | } 311 | 312 | 313 | DB.prototype.user_get = async function (userid) { 314 | let user = null; 315 | return new Promise((resolve, reject) => { 316 | this.db.serialize(() => { 317 | this.db.each("SELECT userid, username, pubkey from users WHERE userid = (?)", [userid], (err, row) => { 318 | if (err) { 319 | reject(err); 320 | } else { 321 | user = { 322 | userid: row.userid, 323 | username: row.username, 324 | pubkey: row.pubkey 325 | }; 326 | } 327 | }, (err) => { 328 | if (err) { 329 | reject(err); 330 | } else { 331 | resolve(user); 332 | } 333 | }); 334 | }); 335 | }); 336 | } 337 | 338 | DB.prototype.user_getPrivateKey = async function (userid) { 339 | let privkey = null; 340 | return new Promise((resolve, reject) => { 341 | this.db.serialize(() => { 342 | this.db.each("SELECT privkey from users WHERE userid = (?)", [userid], (err, row) => { 343 | if (err) { 344 | reject(err); 345 | } else { 346 | privkey = row.privkey; 347 | } 348 | }, (err) => { 349 | if (err) { 350 | reject(err); 351 | } else { 352 | resolve(privkey); 353 | } 354 | }); 355 | }); 356 | }); 357 | } 358 | 359 | DB.prototype.user_all = async function () { 360 | let users = []; 361 | return new Promise((resolve, reject) => { 362 | this.db.serialize(() => { 363 | this.db.each("SELECT userid, username, pwhash, pubkey from users", [], (err, row) => { 364 | if (err) { 365 | reject(err); 366 | } else { 367 | users.push({ 368 | userid: row.userid, 369 | username: row.username, 370 | pwhash: row.pwhash, 371 | pubkey: row.pubkey 372 | }); 373 | } 374 | }, (err) => { 375 | if (err) { 376 | reject(err); 377 | } else { 378 | resolve(users); 379 | } 380 | }); 381 | }); 382 | }); 383 | } 384 | 385 | DB.prototype.table_list = async function () { 386 | let tables = []; 387 | return new Promise((resolve, reject) => { 388 | this.db.serialize(() => { 389 | this.db.each("SELECT name FROM sqlite_schema WHERE type ='table' AND name NOT LIKE 'sqlite_%';", [], (err, row) => { 390 | if (err) { 391 | reject(err); 392 | } else { 393 | tables.push(row.name); 394 | } 395 | }, (err) => { 396 | if (err) { 397 | reject(err); 398 | } else { 399 | resolve(tables); 400 | } 401 | }); 402 | }); 403 | }); 404 | } 405 | 406 | DB.prototype.token_removeCode = async function (token) { 407 | return new Promise((resolve, reject) => { 408 | this.db.serialize(() => { 409 | this.db.run("UPDATE tokens SET code = NULL WHERE token = (?)", [token], (err, row) => { 410 | if (err) { 411 | reject(err); 412 | } else { 413 | resolve(); 414 | } 415 | }); 416 | }); 417 | }); 418 | } 419 | 420 | DB.prototype.token_getByCode = async function (code) { 421 | let token = null; 422 | return new Promise((resolve, reject) => { 423 | this.db.serialize(() => { 424 | this.db.each("SELECT token, code, created_at, userid, scope, deleteable from tokens WHERE code = (?)", [code], (err, row) => { 425 | if (err) { 426 | reject(err); 427 | } else { 428 | token = { 429 | userid: row.userid, 430 | created_at: row.created_at, 431 | code: row.code, 432 | token: row.token, 433 | scope: row.scope, 434 | deleteable: row.deleteable > 0 435 | }; 436 | } 437 | }, (err) => { 438 | if (err) { 439 | reject(err); 440 | } else { 441 | resolve(token); 442 | } 443 | }); 444 | }); 445 | }); 446 | } 447 | 448 | DB.prototype.token_all = async function () { 449 | let tokens = []; 450 | return new Promise((resolve, reject) => { 451 | this.db.serialize(() => { 452 | this.db.each("SELECT token, code, created_at, userid, scope, deleteable from tokens", [], (err, row) => { 453 | if (err) { 454 | reject(err); 455 | } else { 456 | tokens.push({ 457 | userid: row.userid, 458 | created_at: row.created_at, 459 | code: row.code, 460 | token: row.token, 461 | scope: row.scope, 462 | deleteable: row.deleteable > 0 463 | }); 464 | } 465 | }, (err) => { 466 | if (err) { 467 | reject(err); 468 | } else { 469 | resolve(tokens); 470 | } 471 | }); 472 | }); 473 | }); 474 | } 475 | 476 | DB.prototype.token_add = async function (user, scope, undeleteable) { 477 | let deleteable; 478 | if (undeleteable === undefined) { 479 | deleteable = false; 480 | } else { 481 | deleteable = !undeleteable; 482 | } 483 | 484 | return new Promise((resolve, reject) => { 485 | let db = this.db; 486 | 487 | crypto.randomBytes(TOKENLEN, function(err, buffer) { 488 | let code = buffer.toString('hex'); 489 | crypto.randomBytes(TOKENLEN, function(err, buffer) { 490 | let token = buffer.toString('hex'); 491 | db.serialize(() => { 492 | db.run("INSERT INTO tokens (userid, token, created_at, code, scope, deleteable) VALUES(?,?,?,?,?,?)", [user.userid, token, Date.now(), code, scope, deleteable ? 1 : 0], (err) => { 493 | if (err) { 494 | reject(err); 495 | } else { 496 | } 497 | }, (err) => { 498 | if (err) { 499 | reject(err); 500 | } else { 501 | let rettoken = null; 502 | db.each("SELECT userid, token, created_at, code, scope, deleteable from tokens WHERE token == (?)", [token], (err, row) => { 503 | rettoken = { 504 | userid: row.userid, 505 | token: row.token, 506 | code: row.code, 507 | scope: row.token, 508 | created_at: row.created_at, 509 | deleteable: row.deleteable ? true : false 510 | }; 511 | }, (err) => { 512 | if (err) { 513 | reject(err); 514 | } else { 515 | resolve(rettoken); 516 | } 517 | }); 518 | } 519 | }); 520 | }); 521 | }); 522 | }); 523 | }); 524 | } 525 | 526 | DB.prototype.token_delStale = async function () { 527 | return new Promise((resolve, reject) => { 528 | this.db.serialize(() => { 529 | this.db.run("DELETE FROM tokens WHERE code IS NOT NULL AND deleteable != 0 AND ((?) - created_at) > 30000", [Date.now()], (err) => { 530 | if (err) { 531 | reject(err); 532 | } else { 533 | } 534 | }, (err) => { 535 | if (err) { 536 | reject(err); 537 | } else { 538 | resolve(); 539 | } 540 | }); 541 | }); 542 | }); 543 | } 544 | 545 | DB.prototype.token_del = async function (token) { 546 | return new Promise((resolve, reject) => { 547 | this.db.serialize(() => { 548 | this.db.run("DELETE FROM tokens WHERE token == (?) AND deleteable != 0", [token], (err) => { 549 | if (err) { 550 | reject(err); 551 | } else { 552 | } 553 | }, (err) => { 554 | if (err) { 555 | reject(err); 556 | } else { 557 | resolve(); 558 | } 559 | }); 560 | }); 561 | }); 562 | } 563 | 564 | DB.prototype.token_delByUser = async function (user) { 565 | return new Promise((resolve, reject) => { 566 | this.db.serialize(() => { 567 | this.db.run("DELETE FROM tokens WHERE userid == (?)", [user.userid], (err) => { 568 | if (err) { 569 | reject(err); 570 | } else { 571 | } 572 | }, (err) => { 573 | if (err) { 574 | reject(err); 575 | } else { 576 | resolve(); 577 | } 578 | }); 579 | }); 580 | }); 581 | } 582 | 583 | DB.prototype.follower_add = async function (userid, actor) { 584 | return new Promise((resolve, reject) => { 585 | let db = this.db; 586 | db.serialize(() => { 587 | db.run("INSERT OR REPLACE INTO followers (userid, actor, followed_at) VALUES(?,?,?)", [userid, actor, Date.now()], (err) => { 588 | if (err) { 589 | reject(err); 590 | } else { 591 | } 592 | }, (err) => { 593 | if (err) { 594 | reject(err); 595 | } else { 596 | resolve(); 597 | } 598 | }); 599 | }); 600 | }); 601 | } 602 | 603 | DB.prototype.follower_del = async function (userid, actor) { 604 | return new Promise((resolve, reject) => { 605 | let db = this.db; 606 | db.serialize(() => { 607 | db.run("DELETE FROM followers WHERE userid == (?) AND actor == (?)", [userid, actor], (err) => { 608 | if (err) { 609 | reject(err); 610 | } else { 611 | } 612 | }, (err) => { 613 | if (err) { 614 | reject(err); 615 | } else { 616 | resolve(); 617 | } 618 | }); 619 | }); 620 | }); 621 | } 622 | 623 | DB.prototype.follower_delAllForUser = async function (userid) { 624 | return new Promise((resolve, reject) => { 625 | let db = this.db; 626 | db.serialize(() => { 627 | db.run("DELETE FROM followers WHERE userid == (?)", [userid], (err) => { 628 | if (err) { 629 | reject(err); 630 | } else { 631 | } 632 | }, (err) => { 633 | if (err) { 634 | reject(err); 635 | } else { 636 | resolve(); 637 | } 638 | }); 639 | }); 640 | }); 641 | } 642 | 643 | DB.prototype.follower_allForUser = async function (userid) { 644 | let followers = []; 645 | return new Promise((resolve, reject) => { 646 | this.db.serialize(() => { 647 | this.db.each("SELECT actor, followed_at FROM followers WHERE userid == (?)", [userid], (err, row) => { 648 | if (err) { 649 | reject(err); 650 | } else { 651 | followers.push({ 652 | actor: row.actor, 653 | followed_at: row.followed_at 654 | }); 655 | } 656 | }, (err) => { 657 | if (err) { 658 | reject(err); 659 | } else { 660 | resolve(followers); 661 | } 662 | }); 663 | }); 664 | }); 665 | } 666 | 667 | DB.prototype.actor_add = async function (actor, json) { 668 | return new Promise((resolve, reject) => { 669 | let db = this.db; 670 | db.serialize(() => { 671 | db.run("INSERT OR REPLACE INTO actors (actor, json, created_at) VALUES(?,?,?)", [actor, JSON.stringify(json), Date.now()], (err) => { 672 | if (err) { 673 | reject(err); 674 | } else { 675 | } 676 | }, (err) => { 677 | if (err) { 678 | reject(err); 679 | } else { 680 | resolve(); 681 | } 682 | }); 683 | }); 684 | }); 685 | } 686 | 687 | DB.prototype.actor_getList = async function () { 688 | let actorList = []; 689 | return new Promise((resolve, reject) => { 690 | this.db.serialize(() => { 691 | this.db.each("SELECT actor FROM actors", [], (err, row) => { 692 | if (err) { 693 | reject(err); 694 | } else { 695 | actorList.push(row.actor); 696 | } 697 | }, (err) => { 698 | if (err) { 699 | reject(err); 700 | } else { 701 | resolve(actorList); 702 | } 703 | }); 704 | }); 705 | }); 706 | } 707 | 708 | DB.prototype.actor_get = async function (actor) { 709 | let actorData = null; 710 | return new Promise((resolve, reject) => { 711 | this.db.serialize(() => { 712 | this.db.each("SELECT actor, json, created_at FROM actors WHERE actor == (?)", [actor], (err, row) => { 713 | if (err) { 714 | reject(err); 715 | } else { 716 | actorData = { 717 | actor: row.actor, 718 | json: JSON.parse(row.json), 719 | created_at: row.created_at 720 | }; 721 | } 722 | }, (err) => { 723 | if (err) { 724 | reject(err); 725 | } else { 726 | resolve(actorData); 727 | } 728 | }); 729 | }); 730 | }); 731 | } 732 | 733 | DB.prototype.actor_delAll = async function (userid) { 734 | return new Promise((resolve, reject) => { 735 | let db = this.db; 736 | db.serialize(() => { 737 | db.run("DELETE FROM actors", [], (err) => { 738 | if (err) { 739 | reject(err); 740 | } else { 741 | } 742 | }, (err) => { 743 | if (err) { 744 | reject(err); 745 | } else { 746 | resolve(); 747 | } 748 | }); 749 | }); 750 | }); 751 | } 752 | 753 | DB.prototype.message_delAll = async function () { 754 | return new Promise((resolve, reject) => { 755 | let db = this.db; 756 | db.serialize(() => { 757 | db.run("DELETE FROM messages", [], (err) => { 758 | if (err) { 759 | reject(err); 760 | } else { 761 | } 762 | }, (err) => { 763 | if (err) { 764 | reject(err); 765 | } else { 766 | resolve(); 767 | } 768 | }); 769 | }); 770 | }); 771 | } 772 | 773 | DB.prototype.message_delByObjectIdAndActor = async function (objectid, actor) { 774 | return new Promise((resolve, reject) => { 775 | let db = this.db; 776 | db.serialize(() => { 777 | db.run("DELETE FROM messages WHERE objectid == (?) AND actor == (?)", [objectid, actor], (err) => { 778 | if (err) { 779 | reject(err); 780 | } else { 781 | } 782 | }, (err) => { 783 | if (err) { 784 | reject(err); 785 | } else { 786 | resolve(); 787 | } 788 | }); 789 | }); 790 | }); 791 | } 792 | 793 | DB.prototype.message_delByObjectId = async function (objectid) { 794 | return new Promise((resolve, reject) => { 795 | let db = this.db; 796 | db.serialize(() => { 797 | db.run("DELETE FROM messages WHERE objectid == (?)", [objectid], (err) => { 798 | if (err) { 799 | reject(err); 800 | } else { 801 | } 802 | }, (err) => { 803 | if (err) { 804 | reject(err); 805 | } else { 806 | resolve(); 807 | } 808 | }); 809 | }); 810 | }); 811 | } 812 | 813 | DB.prototype.message_del = async function (guid) { 814 | return new Promise((resolve, reject) => { 815 | let db = this.db; 816 | db.serialize(() => { 817 | db.run("DELETE FROM messages WHERE guid == (?)", [guid], (err) => { 818 | if (err) { 819 | reject(err); 820 | } else { 821 | } 822 | }, (err) => { 823 | if (err) { 824 | reject(err); 825 | } else { 826 | resolve(); 827 | } 828 | }); 829 | }); 830 | }); 831 | } 832 | 833 | DB.prototype.message_add = async function (guid, message, actor, fixedobjectid, published_at) { 834 | let objectid = null; 835 | if (fixedobjectid !== undefined) { 836 | objectid = fixedobjectid; 837 | } else { 838 | if (message && message.object && message.object.id) { 839 | objectid = message.object.id; // if there is one, pull it out, so record is findable by objectid 840 | } 841 | } 842 | return new Promise((resolve, reject) => { 843 | let db = this.db; 844 | db.serialize(() => { 845 | db.run("INSERT OR REPLACE INTO messages (guid, message, created_at, actor, objectid, published_at) VALUES(?,?,?,?,?,?)", [guid, JSON.stringify(message), Date.now(), actor, objectid, published_at || Date.now()], (err) => { 846 | if (err) { 847 | reject(err); 848 | } else { 849 | } 850 | }, (err) => { 851 | if (err) { 852 | reject(err); 853 | } else { 854 | resolve(); 855 | } 856 | }); 857 | }); 858 | }); 859 | } 860 | 861 | DB.prototype.message_getByObjectId = async function (objectid) { 862 | let msg = null; 863 | return new Promise((resolve, reject) => { 864 | this.db.serialize(() => { 865 | this.db.each("SELECT guid, message, created_at FROM messages WHERE objectid == (?)", [objectid], (err, row) => { 866 | if (err) { 867 | reject(err); 868 | } else { 869 | msg = { 870 | guid: row.guid, 871 | message: JSON.parse(row.message), 872 | created_at: row.created_at, 873 | actor: row.actor, 874 | objectid: row.objectid 875 | }; 876 | } 877 | }, (err) => { 878 | if (err) { 879 | reject(err); 880 | } else { 881 | resolve(msg); 882 | } 883 | }); 884 | }); 885 | }); 886 | } 887 | 888 | 889 | DB.prototype.message_get = async function (guid) { 890 | let msg = null; 891 | return new Promise((resolve, reject) => { 892 | this.db.serialize(() => { 893 | this.db.each("SELECT guid, actor, message, objectid, created_at FROM messages WHERE guid == (?)", [guid], (err, row) => { 894 | if (err) { 895 | reject(err); 896 | } else { 897 | msg = { 898 | guid: row.guid, 899 | message: JSON.parse(row.message), 900 | created_at: row.created_at, 901 | actor: row.actor, 902 | objectid: row.objectid 903 | }; 904 | } 905 | }, (err) => { 906 | if (err) { 907 | reject(err); 908 | } else { 909 | resolve(msg); 910 | } 911 | }); 912 | }); 913 | }); 914 | } 915 | 916 | DB.prototype.message_getList = async function () { 917 | let messageList = []; 918 | return new Promise((resolve, reject) => { 919 | this.db.serialize(() => { 920 | this.db.each("SELECT guid FROM messages ORDER BY created_at", [], (err, row) => { 921 | if (err) { 922 | reject(err); 923 | } else { 924 | messageList.push(row.guid); 925 | } 926 | }, (err) => { 927 | if (err) { 928 | reject(err); 929 | } else { 930 | resolve(messageList); 931 | } 932 | }); 933 | }); 934 | }); 935 | } 936 | 937 | 938 | DB.prototype.following_add = async function (userid, actor) { 939 | return new Promise((resolve, reject) => { 940 | let db = this.db; 941 | db.serialize(() => { 942 | db.run("INSERT OR REPLACE INTO following (userid, actor, followed_at) VALUES(?,?,?)", [userid, actor, Date.now()], (err) => { 943 | if (err) { 944 | reject(err); 945 | } else { 946 | } 947 | }, (err) => { 948 | if (err) { 949 | reject(err); 950 | } else { 951 | resolve(); 952 | } 953 | }); 954 | }); 955 | }); 956 | } 957 | 958 | DB.prototype.following_del = async function (userid, actor) { 959 | return new Promise((resolve, reject) => { 960 | let db = this.db; 961 | db.serialize(() => { 962 | db.run("DELETE FROM following WHERE userid == (?) AND actor == (?)", [userid, actor], (err) => { 963 | if (err) { 964 | reject(err); 965 | } else { 966 | } 967 | }, (err) => { 968 | if (err) { 969 | reject(err); 970 | } else { 971 | resolve(); 972 | } 973 | }); 974 | }); 975 | }); 976 | } 977 | 978 | DB.prototype.following_delAllForUser = async function (userid) { 979 | return new Promise((resolve, reject) => { 980 | let db = this.db; 981 | db.serialize(() => { 982 | db.run("DELETE FROM following WHERE userid == (?)", [userid], (err) => { 983 | if (err) { 984 | reject(err); 985 | } else { 986 | } 987 | }, (err) => { 988 | if (err) { 989 | reject(err); 990 | } else { 991 | resolve(); 992 | } 993 | }); 994 | }); 995 | }); 996 | } 997 | 998 | DB.prototype.following_allForUser = async function (userid) { 999 | let following = []; 1000 | return new Promise((resolve, reject) => { 1001 | this.db.serialize(() => { 1002 | this.db.each("SELECT actor, followed_at FROM following WHERE userid == (?)", [userid], (err, row) => { 1003 | if (err) { 1004 | reject(err); 1005 | } else { 1006 | following.push({ 1007 | actor: row.actor, 1008 | followed_at: row.followed_at 1009 | }); 1010 | } 1011 | }, (err) => { 1012 | if (err) { 1013 | reject(err); 1014 | } else { 1015 | resolve(following); 1016 | } 1017 | }); 1018 | }); 1019 | }); 1020 | } 1021 | 1022 | DB.prototype.like_add = async function (guid, actor) { 1023 | return new Promise((resolve, reject) => { 1024 | let db = this.db; 1025 | db.serialize(() => { 1026 | db.run("INSERT OR REPLACE INTO likes (guid, actor, created_at) VALUES(?,?,?)", [guid, actor, Date.now()], (err) => { 1027 | if (err) { 1028 | reject(err); 1029 | } else { 1030 | } 1031 | }, (err) => { 1032 | if (err) { 1033 | reject(err); 1034 | } else { 1035 | resolve(); 1036 | } 1037 | }); 1038 | }); 1039 | }); 1040 | } 1041 | 1042 | DB.prototype.like_del = async function (guid, actor) { 1043 | return new Promise((resolve, reject) => { 1044 | let db = this.db; 1045 | db.serialize(() => { 1046 | db.run("DELETE FROM likes WHERE guid == (?) AND actor == (?)", [guid, actor], (err) => { 1047 | if (err) { 1048 | reject(err); 1049 | } else { 1050 | } 1051 | }, (err) => { 1052 | if (err) { 1053 | reject(err); 1054 | } else { 1055 | resolve(); 1056 | } 1057 | }); 1058 | }); 1059 | }); 1060 | } 1061 | 1062 | DB.prototype.like_getList = async function () { 1063 | let likeList = []; 1064 | return new Promise((resolve, reject) => { 1065 | this.db.serialize(() => { 1066 | this.db.each("SELECT guid, actor FROM likes", [], (err, row) => { 1067 | if (err) { 1068 | reject(err); 1069 | } else { 1070 | likeList.push({ 1071 | guid: row.guid, 1072 | actor: row.actor 1073 | }); 1074 | } 1075 | }, (err) => { 1076 | if (err) { 1077 | reject(err); 1078 | } else { 1079 | resolve(likeList); 1080 | } 1081 | }); 1082 | }); 1083 | }); 1084 | } 1085 | 1086 | DB.prototype.like_check = async function (guid, actor) { 1087 | let liked = false; 1088 | return new Promise((resolve, reject) => { 1089 | this.db.serialize(() => { 1090 | this.db.each("SELECT guid, actor FROM likes WHERE guid == (?) AND actor == (?)", [guid, actor], (err, row) => { 1091 | if (err) { 1092 | reject(err); 1093 | } else { 1094 | liked = true; 1095 | } 1096 | }, (err) => { 1097 | if (err) { 1098 | reject(err); 1099 | } else { 1100 | resolve(liked); 1101 | } 1102 | }); 1103 | }); 1104 | }); 1105 | } 1106 | 1107 | DB.prototype.announce_check = async function (guid, actor) { 1108 | let announced = false; 1109 | return new Promise((resolve, reject) => { 1110 | this.db.serialize(() => { 1111 | this.db.each("SELECT guid, actor FROM announces WHERE guid == (?) AND actor == (?)", [guid, actor], (err, row) => { 1112 | if (err) { 1113 | reject(err); 1114 | } else { 1115 | announced = true; 1116 | } 1117 | }, (err) => { 1118 | if (err) { 1119 | reject(err); 1120 | } else { 1121 | resolve(announced); 1122 | } 1123 | }); 1124 | }); 1125 | }); 1126 | } 1127 | 1128 | 1129 | DB.prototype.announce_add = async function (guid, actor) { 1130 | return new Promise((resolve, reject) => { 1131 | let db = this.db; 1132 | db.serialize(() => { 1133 | db.run("INSERT OR REPLACE INTO announces (guid, actor, created_at) VALUES(?,?,?)", [guid, actor, Date.now()], (err) => { 1134 | if (err) { 1135 | reject(err); 1136 | } else { 1137 | } 1138 | }, (err) => { 1139 | if (err) { 1140 | reject(err); 1141 | } else { 1142 | resolve(); 1143 | } 1144 | }); 1145 | }); 1146 | }); 1147 | } 1148 | 1149 | DB.prototype.announce_delAll = async function () { 1150 | return new Promise((resolve, reject) => { 1151 | let db = this.db; 1152 | db.serialize(() => { 1153 | db.run("DELETE FROM announces", [], (err) => { 1154 | if (err) { 1155 | reject(err); 1156 | } else { 1157 | } 1158 | }, (err) => { 1159 | if (err) { 1160 | reject(err); 1161 | } else { 1162 | resolve(); 1163 | } 1164 | }); 1165 | }); 1166 | }); 1167 | } 1168 | 1169 | DB.prototype.announce_del = async function (guid, actor) { 1170 | return new Promise((resolve, reject) => { 1171 | let db = this.db; 1172 | db.serialize(() => { 1173 | db.run("DELETE FROM announces WHERE guid == (?) AND actor == (?)", [guid, actor], (err) => { 1174 | if (err) { 1175 | reject(err); 1176 | } else { 1177 | } 1178 | }, (err) => { 1179 | if (err) { 1180 | reject(err); 1181 | } else { 1182 | resolve(); 1183 | } 1184 | }); 1185 | }); 1186 | }); 1187 | } 1188 | 1189 | DB.prototype.announce_getList = async function () { 1190 | let announceList = []; 1191 | return new Promise((resolve, reject) => { 1192 | this.db.serialize(() => { 1193 | this.db.each("SELECT guid, actor FROM announces", [], (err, row) => { 1194 | if (err) { 1195 | reject(err); 1196 | } else { 1197 | announceList.push({ 1198 | guid: row.guid, 1199 | actor: row.actor 1200 | }); 1201 | } 1202 | }, (err) => { 1203 | if (err) { 1204 | reject(err); 1205 | } else { 1206 | resolve(announceList); 1207 | } 1208 | }); 1209 | }); 1210 | }); 1211 | } 1212 | 1213 | 1214 | DB.prototype.timeline = async function (userid, user_actor) { 1215 | let messages = []; 1216 | return new Promise((resolve, reject) => { 1217 | this.db.serialize(() => { 1218 | this.db.each("select distinct guid, message, messages.actor as actor, created_at from messages inner join following on ((messages.actor = following.actor) OR (messages.actor = (?))) where userid == (?) order by published_at desc", [user_actor, userid], (err, row) => { 1219 | if (err) { 1220 | reject(err); 1221 | } else { 1222 | messages.push({ 1223 | guid: row.guid, 1224 | actor: row.actor, 1225 | message: JSON.parse(row.message), 1226 | created_at: row.created_at 1227 | }); 1228 | } 1229 | }, (err) => { 1230 | if (err) { 1231 | reject(err); 1232 | } else { 1233 | resolve(messages); 1234 | } 1235 | }); 1236 | }); 1237 | }); 1238 | } 1239 | 1240 | DB.prototype.media_add = async function (guid, userid, file, preview, type, blurhash, description, meta) { 1241 | return new Promise((resolve, reject) => { 1242 | let db = this.db; 1243 | let created_at = Date.now(); 1244 | db.serialize(() => { 1245 | db.run("INSERT OR REPLACE INTO medias (created_at, guid, userid, file, preview, type, blurhash, description, meta) VALUES(?,?,?,?,?,?,?,?,?)", [created_at, guid, userid, file, preview, type, blurhash, description, meta], (err) => { 1246 | if (err) { 1247 | reject(err); 1248 | } else { 1249 | } 1250 | }, (err) => { 1251 | if (err) { 1252 | reject(err); 1253 | } else { 1254 | resolve({ 1255 | guid: guid, 1256 | userid: userid, 1257 | created_at: created_at, 1258 | file: file, 1259 | preview: preview, 1260 | type: type, 1261 | blurhash: blurhash, 1262 | description: description, 1263 | meta: meta 1264 | }); 1265 | } 1266 | }); 1267 | }); 1268 | }); 1269 | } 1270 | 1271 | DB.prototype.media_delAll = async function (guid) { 1272 | return new Promise((resolve, reject) => { 1273 | let db = this.db; 1274 | db.serialize(() => { 1275 | db.run("DELETE FROM medias", [], (err) => { 1276 | if (err) { 1277 | reject(err); 1278 | } else { 1279 | } 1280 | }, (err) => { 1281 | if (err) { 1282 | reject(err); 1283 | } else { 1284 | resolve(); 1285 | } 1286 | }); 1287 | }); 1288 | }); 1289 | } 1290 | 1291 | 1292 | DB.prototype.media_del = async function (guid) { 1293 | return new Promise((resolve, reject) => { 1294 | let db = this.db; 1295 | db.serialize(() => { 1296 | db.run("DELETE FROM medias WHERE guid == (?)", [guid], (err) => { 1297 | if (err) { 1298 | reject(err); 1299 | } else { 1300 | } 1301 | }, (err) => { 1302 | if (err) { 1303 | reject(err); 1304 | } else { 1305 | resolve(); 1306 | } 1307 | }); 1308 | }); 1309 | }); 1310 | } 1311 | 1312 | DB.prototype.media_get = async function (guid) { 1313 | let media; 1314 | return new Promise((resolve, reject) => { 1315 | this.db.serialize(() => { 1316 | this.db.each("SELECT guid, userid, created_at, file, preview, type, blurhash, description, meta FROM medias WHERE guid = (?)", [guid], (err, row) => { 1317 | if (err) { 1318 | reject(err); 1319 | } else { 1320 | media = { 1321 | guid: row.guid, 1322 | userid: row.userid, 1323 | created_at: row.created_at, 1324 | file: row.file, 1325 | preview: row.preview, 1326 | type: row.type, 1327 | blurhash: row.blurhash, 1328 | description: row.description, 1329 | meta: row.meta 1330 | }; 1331 | } 1332 | }, (err) => { 1333 | if (err) { 1334 | reject(err); 1335 | } else { 1336 | resolve(media); 1337 | } 1338 | }); 1339 | }); 1340 | }); 1341 | } 1342 | 1343 | 1344 | DB.prototype.media_getList = async function () { 1345 | let mediaList = []; 1346 | return new Promise((resolve, reject) => { 1347 | this.db.serialize(() => { 1348 | this.db.each("SELECT guid, userid, created_at, file, preview, type, blurhash, description, meta FROM medias", [], (err, row) => { 1349 | if (err) { 1350 | reject(err); 1351 | } else { 1352 | mediaList.push(row.guid); 1353 | } 1354 | }, (err) => { 1355 | if (err) { 1356 | reject(err); 1357 | } else { 1358 | resolve(mediaList); 1359 | } 1360 | }); 1361 | }); 1362 | }); 1363 | } 1364 | 1365 | module.exports = DB; 1366 | -------------------------------------------------------------------------------- /cubiti-server/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | (cd /build && node server.js) 3 | -------------------------------------------------------------------------------- /cubiti-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "bluebird": "^3.7.2", 4 | "express": "^4.17.1", 5 | "body-parser": "^1.20.1", 6 | "ws": "^8.11.0", 7 | "sqlite3": "^5.1.4", 8 | "vorpal": "^1.12.0", 9 | "bcrypt": "^5.1.0", 10 | "passport": "^0.6.0", 11 | "passport-bearer-strategy": "^1.0.1", 12 | "express-session": "^1.17.3", 13 | "cookie-parser": "^1.4.6", 14 | "passport-local": "^1.0.0", 15 | "connect-ensure-login": "^0.1.1", 16 | "ejs": "^3.1.8", 17 | "cors": "^2.8.5", 18 | "request": "^2.88.2", 19 | "url-safe-base64": "^1.2.0", 20 | "node-fetch": "^2.6.7", 21 | "formidable": "^2.1.1", 22 | "image-size": "^1.0.2", 23 | "blurhash": "^2.0.4", 24 | "@andreekeberg/imagedata": "^1.0.2", 25 | "redis": "^4.5.1" 26 | }, 27 | "scripts": { 28 | "start": "node server.js" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cubiti-server/public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringtailsoftware/cubiti/ba22c248d1cc792d358682d01028d15e701d9da6/cubiti-server/public/.DS_Store -------------------------------------------------------------------------------- /cubiti-server/public/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Thibaut Courouble 3 | * http://www.cssflow.com 4 | * Licensed under the MIT License 5 | * 6 | * Sass/SCSS source: https://goo.gl/0jzXf 7 | * PSD by Orman Clark: https://goo.gl/D8zmk 8 | */ 9 | 10 | body { 11 | font: 13px/20px "Lucida Grande", Tahoma, Verdana, sans-serif; 12 | color: #404040; 13 | background: #E0E0E0; 14 | } 15 | 16 | .login { 17 | position: relative; 18 | margin: 30px auto; 19 | padding: 20px 20px 20px; 20 | width: 310px; 21 | background: white; 22 | border-radius: 3px; 23 | -webkit-box-shadow: 0 0 200px rgba(255, 255, 255, 0.5), 0 1px 2px rgba(0, 0, 0, 0.3); 24 | box-shadow: 0 0 200px rgba(255, 255, 255, 0.5), 0 1px 2px rgba(0, 0, 0, 0.3); 25 | } 26 | 27 | .login:before { 28 | content: ''; 29 | position: absolute; 30 | top: -8px; 31 | right: -8px; 32 | bottom: -8px; 33 | left: -8px; 34 | z-index: -1; 35 | background: rgba(0, 0, 0, 0.08); 36 | border-radius: 4px; 37 | } 38 | 39 | .login h1 { 40 | margin: -20px -20px 21px; 41 | line-height: 40px; 42 | font-size: 15px; 43 | font-weight: bold; 44 | color: #555; 45 | text-align: center; 46 | text-shadow: 0 1px white; 47 | background: #f3f3f3; 48 | border-bottom: 1px solid #cfcfcf; 49 | border-radius: 3px 3px 0 0; 50 | background-image: -webkit-linear-gradient(top, whiteffd, #eef2f5); 51 | background-image: -moz-linear-gradient(top, whiteffd, #eef2f5); 52 | background-image: -o-linear-gradient(top, whiteffd, #eef2f5); 53 | background-image: linear-gradient(to bottom, whiteffd, #eef2f5); 54 | -webkit-box-shadow: 0 1px whitesmoke; 55 | box-shadow: 0 1px whitesmoke; 56 | } 57 | 58 | .login p { 59 | margin: 20px 0 0; 60 | } 61 | 62 | .login p:first-child { 63 | margin-top: 0; 64 | } 65 | 66 | .login input[type=text], .login input[type=password] { 67 | width: 278px; 68 | } 69 | 70 | .login p.remember_me { 71 | float: left; 72 | line-height: 31px; 73 | } 74 | 75 | .login p.remember_me label { 76 | font-size: 12px; 77 | color: #777; 78 | cursor: pointer; 79 | } 80 | 81 | .login p.remember_me input { 82 | position: relative; 83 | bottom: 1px; 84 | margin-right: 4px; 85 | vertical-align: middle; 86 | } 87 | 88 | .login p.submit { 89 | text-align: right; 90 | } 91 | 92 | .login-help { 93 | margin: 20px 0; 94 | font-size: 11px; 95 | color: white; 96 | text-align: center; 97 | text-shadow: 0 1px #2a85a1; 98 | } 99 | 100 | .login-help a { 101 | color: #cce7fa; 102 | text-decoration: none; 103 | } 104 | 105 | .login-help a:hover { 106 | text-decoration: underline; 107 | } 108 | 109 | :-moz-placeholder { 110 | color: #c9c9c9 !important; 111 | font-size: 13px; 112 | } 113 | 114 | ::-webkit-input-placeholder { 115 | color: #ccc; 116 | font-size: 13px; 117 | } 118 | 119 | input { 120 | font-family: 'Lucida Grande', Tahoma, Verdana, sans-serif; 121 | font-size: 14px; 122 | } 123 | 124 | input[type=text], input[type=password] { 125 | margin: 5px; 126 | padding: 0 10px; 127 | width: 200px; 128 | height: 34px; 129 | color: #404040; 130 | background: white; 131 | border: 1px solid; 132 | border-color: #c4c4c4 #d1d1d1 #d4d4d4; 133 | border-radius: 2px; 134 | outline: 5px solid #eff4f7; 135 | -moz-outline-radius: 3px; 136 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.12); 137 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.12); 138 | } 139 | 140 | input[type=text]:focus, input[type=password]:focus { 141 | border-color: #7dc9e2; 142 | outline-color: #dceefc; 143 | outline-offset: 0; 144 | } 145 | 146 | input[type=submit] { 147 | padding: 0 18px; 148 | height: 29px; 149 | font-size: 12px; 150 | font-weight: bold; 151 | color: #527881; 152 | text-shadow: 0 1px #e3f1f1; 153 | background: #cde5ef; 154 | border: 1px solid; 155 | border-color: #b4ccce #b3c0c8 #9eb9c2; 156 | border-radius: 16px; 157 | outline: 0; 158 | -webkit-box-sizing: content-box; 159 | -moz-box-sizing: content-box; 160 | box-sizing: content-box; 161 | background-image: -webkit-linear-gradient(top, #edf5f8, #cde5ef); 162 | background-image: -moz-linear-gradient(top, #edf5f8, #cde5ef); 163 | background-image: -o-linear-gradient(top, #edf5f8, #cde5ef); 164 | background-image: linear-gradient(to bottom, #edf5f8, #cde5ef); 165 | -webkit-box-shadow: inset 0 1px white, 0 1px 2px rgba(0, 0, 0, 0.15); 166 | box-shadow: inset 0 1px white, 0 1px 2px rgba(0, 0, 0, 0.15); 167 | } 168 | 169 | input[type=submit]:active { 170 | background: #cde5ef; 171 | border-color: #9eb9c2 #b3c0c8 #b4ccce; 172 | -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.2); 173 | box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.2); 174 | } 175 | 176 | .lt-ie9 input[type=text], .lt-ie9 input[type=password] { 177 | line-height: 34px; 178 | } 179 | -------------------------------------------------------------------------------- /cubiti-server/public/images/cubiti-logo-cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringtailsoftware/cubiti/ba22c248d1cc792d358682d01028d15e701d9da6/cubiti-server/public/images/cubiti-logo-cropped.png -------------------------------------------------------------------------------- /cubiti-server/public/images/cubiti-logo-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringtailsoftware/cubiti/ba22c248d1cc792d358682d01028d15e701d9da6/cubiti-server/public/images/cubiti-logo-icon.png -------------------------------------------------------------------------------- /cubiti-server/public/images/cubiti-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringtailsoftware/cubiti/ba22c248d1cc792d358682d01028d15e701d9da6/cubiti-server/public/images/cubiti-logo.png -------------------------------------------------------------------------------- /cubiti-server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

8 | cubiti 9 |

10 |

11 | Login to cubiti using Pinafore 12 |

13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /cubiti-server/routes/activitypub.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let express = require('express'); 4 | let router = express.Router(); 5 | let fs = require('fs'); 6 | let Promise = require('bluebird'); 7 | let passport = require('passport'); 8 | var ensureLogIn = require('connect-ensure-login').ensureLoggedIn; 9 | let request = require('request'); 10 | var safeb64 = require('url-safe-base64'); 11 | var path = require('path'); 12 | var urlparser = require('url'); 13 | var fetch = require('node-fetch'); 14 | const crypto = require('crypto'); 15 | let util = require('../util.js'); 16 | 17 | function createWebfinger(username, domain) { 18 | return { 19 | 'subject': `acct:${username}@${domain}`, 20 | 'links': [ 21 | { 22 | 'rel': 'self', 23 | 'type': 'application/activity+json', 24 | 'href': `https://${domain}/u/${username}` 25 | } 26 | ] 27 | }; 28 | } 29 | 30 | router.get('/u/:username', (req, res, next) => { 31 | console.log(`GET /u/${req.params.username} (actor)`); 32 | req.app.db.user_getByName(req.params.username).then((user) => { 33 | if (!user) { 34 | return res.status(404).send(); 35 | } else { 36 | res.setHeader('Content-Type', 'application/activity+json'); 37 | return res.status(200).send(util.createActor(user.username, req.app.config.server.domain, user.pubkey)); 38 | } 39 | }); 40 | }); 41 | 42 | router.get('/.well-known/webfinger', (req, res, next) => { 43 | console.log(`GET /.well-known/webfinger ${req.query.resource}`); 44 | 45 | let resource = req.query.resource; 46 | if (!resource || !resource.startsWith('acct:')) { 47 | return res.status(400).send(); 48 | } 49 | 50 | let [username, domain] = resource.replace('acct:', '').split('@'); 51 | if (!username || !domain) { 52 | return res.status(400).send(); 53 | } 54 | 55 | if (domain != req.app.config.server.domain) { 56 | return res.status(404).send(); 57 | } 58 | 59 | req.app.db.user_getByName(username).then((user) => { 60 | if (!user) { 61 | res.status(404).send(); 62 | } else { 63 | res.setHeader('Content-Type', 'application/activity+json'); 64 | res.status(200).send(createWebfinger(username, domain)); 65 | } 66 | }).catch((err) => { 67 | res.status(500).send(); 68 | }); 69 | }); 70 | 71 | function sendAcceptMessage(app, user, res, object, actor) { 72 | return util.generateGuid().then((guid) => { 73 | let msg = { 74 | '@context': 'https://www.w3.org/ns/activitystreams', 75 | 'id': `https://${app.config.server.domain}/${guid}`, 76 | 'type': 'Accept', 77 | 'actor': actor, 78 | 'object': object 79 | }; 80 | return app.cmd.signObjectAndSend(app, user, msg, msg.object.actor); 81 | }); 82 | } 83 | 84 | router.post('/u/:username/inbox', (req, res, next) => { 85 | let username = req.params.username; 86 | console.log(`POST /u/${username}/inbox`/*, req.headers, req.body*/); 87 | 88 | req.app.db.user_getByNamePrivKey(username).then((user) => { 89 | if (!user) { 90 | return res.status(404).send(); 91 | } else { 92 | let actor = req.body.actor; 93 | let object = req.body.object; 94 | let type = req.body.type; 95 | 96 | console.log(`${type} ${username} ${actor}`); 97 | 98 | // FIXME FIXME FIXME MESSAGE DIGEST CHECK! 99 | 100 | if (type === 'Follow' && typeof object === 'string') { 101 | req.app.db.follower_add(user.userid, actor).then(() => { 102 | return req.app.ev.send('follow', user.userid, null, actor); 103 | }).then(() => { 104 | sendAcceptMessage(req.app, user, res, req.body, object).then(() => { 105 | console.log("** ACCEPT OK"); 106 | res.status(200).send(); 107 | }).catch((err) => { 108 | console.log("** ACCEPT ERR", err); 109 | res.status(500).send(err); 110 | }); 111 | }).catch((err) => { 112 | console.log("ERROR Follower add", err); 113 | res.status(500).send(err); 114 | }); 115 | } else if (type === 'Undo') { 116 | console.log(req.body); 117 | if (typeof object === 'object' && object.type === 'Follow') { 118 | console.log("Undo Follow"); 119 | req.app.db.follower_del(user.userid, actor).then(() => { 120 | return req.app.ev.send('unfollow', user.userid, null, actor); 121 | }).then(() => { 122 | sendAcceptMessage(req.app, user, res, req.body, object.actor).then(() => { 123 | console.log("** ACCEPT OK"); 124 | res.status(200).send(); 125 | }).catch((err) => { 126 | console.log("** ACCEPT ERR", err); 127 | res.status(500).send(err); 128 | }); 129 | }).catch((err) => { 130 | console.log("ERROR Follower del", err); 131 | res.status(500).send(err); 132 | }); 133 | } else if (typeof object === 'object' && object.type === 'Like') { 134 | console.log("Undo Like"); 135 | req.app.db.like_del(object.object, actor).then(() => { 136 | return req.app.ev.send('unlike', user.userid, object, actor); 137 | }).then(() => { 138 | sendAcceptMessage(req.app, user, res, req.body, object.actor).then(() => { 139 | console.log("** ACCEPT OK"); 140 | res.status(200).send(); 141 | }).catch((err) => { 142 | console.log("** ACCEPT ERR", err); 143 | res.status(500).send(err); 144 | }); 145 | }).catch((err) => { 146 | console.log("ERROR Like del", err); 147 | res.status(500).send(err); 148 | }); 149 | } else if (typeof object === 'object' && object.type === 'Announce') { 150 | console.log("Undo Announce"); 151 | req.app.db.announce_del(object.object, actor).then(() => { 152 | return req.app.db.message_del(object.id); // delete the Announce message 153 | }).then(() => { 154 | return req.app.ev.send('unannounce', user.userid, object.id, actor); 155 | }).then(() => { 156 | sendAcceptMessage(req.app, user, res, req.body, object.actor).then(() => { 157 | console.log("** ACCEPT OK"); 158 | res.status(200).send(); 159 | }).catch((err) => { 160 | console.log("** ACCEPT ERR", err); 161 | res.status(500).send(err); 162 | }); 163 | }).catch((err) => { 164 | console.log("ERROR Announce del", err); 165 | res.status(500).send(err); 166 | }); 167 | } 168 | } else if (type === 'Accept') { 169 | res.status(200).send(); 170 | } else if (type === 'Delete') { 171 | // FIXME FIXME FIXME, this really needs to be permission checked 172 | // currently anyone can delete anything (and without sig check) 173 | req.app.db.message_delByObjectId(object.id).then(() => { 174 | return req.app.ev.send('delete', user.userid, object.id, actor); 175 | }).then(() => { 176 | res.status(200).send(); 177 | }).catch((err) => { 178 | console.log("ERROR Delete del msg", err); 179 | res.status(500).send(err); 180 | }); 181 | } else if (type === 'Create') { 182 | if (object.type === 'Note') { 183 | // FIXME, sanity check these before db write 184 | req.app.db.message_add(req.body.object.id, req.body, req.body.actor, new Date(req.body.object.published).getTime()).then(() => { 185 | return req.app.ev.send('update', user.userid, req.body.object.id, req.body.actor); 186 | }).then(() => { 187 | return sendAcceptMessage(req.app, user, res, req.body, object); 188 | }).then(() => { 189 | console.log("** ACCEPT OK"); 190 | res.status(200).send(); 191 | }).catch((err) => { 192 | console.log("** ACCEPT ERR", err); 193 | res.status(500).send(err); 194 | }); 195 | } else { 196 | console.log(req.body); 197 | console.log("** Received Create type=" + object.type); 198 | // FIXME Accept it? 199 | res.status(200).send(); 200 | } 201 | } else if (type === 'Like') { 202 | // FIXME, sanity check these before db write 203 | req.app.db.like_add(object, actor).then(() => { 204 | res.status(200).send(); 205 | }).then(() => { 206 | return req.app.ev.send('like', user.userid, object, actor); 207 | }).catch((err) => { 208 | console.log("Like ERR", err); 209 | res.status(500).send(err); 210 | }); 211 | } else if (type === 'Announce') { 212 | let pb; 213 | util.patchObject(req.app, req.body).then((patchedBody) => { 214 | pb = patchedBody; 215 | // FIXME, sanity check these before db write 216 | req.app.db.message_add(req.body.id, patchedBody, req.body.actor, new Date(req.body.published).getTime()).then(() => { 217 | return req.app.db.announce_add(object, actor); 218 | }).then(() => { 219 | // FIXME using pb decide if status update or boost 220 | if (pb.object.actor == util.userActor(req.app, user)) { 221 | // we're being reblogged 222 | return req.app.ev.send('announce', user.userid, object, actor); 223 | } else { 224 | // someone else is being reblogged 225 | return req.app.ev.send('updateannounce', user.userid, object, actor); 226 | } 227 | }).then(() => { 228 | res.status(200).send(); 229 | }).catch((err) => { 230 | console.log("Announce ERR", err); 231 | res.status(500).send(err); 232 | }); 233 | 234 | }); 235 | 236 | } else { 237 | res.status(500).send(); // unhandled 238 | } 239 | } 240 | }).catch((err) => { 241 | res.status(500).send(); 242 | }); 243 | }); 244 | 245 | //If ids were actually resoveable 246 | /* 247 | router.get('/m/:guid', (req, res, next) => { 248 | console.log(`GET /m/${req.params.guid}`); 249 | // FIXME look at permissions on message and make decisions about visibility and auth 250 | req.app.db.message_get(`https://${req.app.config.server.domain}/m/${req.params.guid}`).then((messageData) => { 251 | if (!messageData) { 252 | res.status(404).send(); 253 | } else { 254 | res.setHeader('Content-Type', 'application/activity+json'); 255 | res.status(200).send(messageData.message); 256 | } 257 | }).catch((err) => { 258 | res.status(500).send(); 259 | }); 260 | }); 261 | 262 | router.get('/m/:guid/object', (req, res, next) => { 263 | console.log(`GET /m/${req.params.guid}`); 264 | // FIXME look at permissions on message and make decisions about visibility and auth 265 | req.app.db.message_get(`https://${req.app.config.server.domain}/m/${req.params.guid}`).then((messageData) => { 266 | if (!messageData) { 267 | res.status(404).send(); 268 | } else { 269 | res.setHeader('Content-Type', 'application/activity+json'); 270 | res.status(200).send(messageData.message.object); 271 | } 272 | }).catch((err) => { 273 | res.status(500).send(); 274 | }); 275 | }); 276 | */ 277 | 278 | ///////////// catch-all routes 279 | 280 | 281 | /* 282 | router.get('*', (req, res, next) => { 283 | console.log('catchall GET ', req.path, req.headers['user-agent']);//, req.params, req.query, req.body); 284 | res.status(410).send(); 285 | }); 286 | 287 | router.post('*', (req, res, next) => { 288 | console.log('catchall POST ', req.path, req.headers['user-agent']);//, req.params, req.query, req.body); 289 | res.status(410).send(); 290 | }); 291 | */ 292 | 293 | 294 | module.exports = router; 295 | 296 | -------------------------------------------------------------------------------- /cubiti-server/routes/mastodon.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let express = require('express'); 4 | let router = express.Router(); 5 | let fs = require('fs'); 6 | let Promise = require('bluebird'); 7 | let passport = require('passport'); 8 | var ensureLogIn = require('connect-ensure-login').ensureLoggedIn; 9 | let request = require('request'); 10 | var path = require('path'); 11 | var urlparser = require('url'); 12 | var fetch = require('node-fetch'); 13 | var util = require('../util.js'); 14 | const formidable = require('formidable'); 15 | const imageSize = require('image-size'); 16 | 17 | 18 | var ensureLoggedIn = ensureLogIn('/login'); 19 | 20 | 21 | router.get('/login', (req, res, next) => { 22 | res.render('login.ejs'); 23 | }); 24 | 25 | router.get('/logout', (req, res, next) => { 26 | req.logout(() => { 27 | res.redirect('/login'); 28 | }); 29 | }); 30 | 31 | router.post('/login/password', passport.authenticate('local', { keepSessionInfo: true, failureRedirect: '/login', successReturnToOrRedirect: '/' }), (req, res, next) => { 32 | console.log("/login/password"); 33 | }); 34 | 35 | router.post('/api/v1/apps', (req, res, next) => { 36 | console.log('/api/v1/apps (POST) ', req.body); 37 | 38 | let rsp = { 39 | "id":"1", 40 | "name": req.body.client_name, 41 | "website": req.body.website, 42 | redirect_uri: req.body.redirect_uris, 43 | "client_id":"qkBfCFV-XhuxR-0-yHNPqgfGAzyPlgzaFqQWgMXWMXE", // don't care 44 | "client_secret":"ZSH1Z8fQdElnQs1cLBti41OE1LAY7dsjTS0eEEt_edo", // don't care 45 | "vapid_key":"BNVo5m_c_MqMVd9yr-yb_iRHqkLi-oJx10JAtrY19t3HEriCc788RRD6-RYjV01NdXVLiqCovJ4M8IuTDUUfyyo=" // don't care 46 | } 47 | 48 | res.status(200).send(rsp); 49 | }); 50 | 51 | router.get('/oauth/authorize', ensureLoggedIn, (req, res, next) => { 52 | // create a token and return the code 53 | req.app.db.token_add({userid: req.user.userid}, req.query.scope, false).then((token) => { 54 | res.redirect(302, req.query.redirect_uri + '?code=' + token.code); 55 | }); 56 | 57 | }); 58 | 59 | router.post('/oauth/token', (req, res, next) => { 60 | // fetch the token by code 61 | req.app.db.token_getByCode(req.body.code).then((token) => { 62 | if (token === null) { 63 | res.status(401).send(); 64 | } else { 65 | // mark token as live (collected over oauth) 66 | req.app.db.token_removeCode(token.token).then(() => { 67 | // remove this and any other stale tokens (partially avoids having a background cleaner process) 68 | return req.app.db.token_delStale(); 69 | }).then(() => { 70 | let rsp = { 71 | "access_token": token.token, 72 | "token_type": "Bearer", 73 | "scope": token.scope, 74 | "created_at": token.created_at 75 | }; 76 | res.status(200).send(rsp); 77 | }); 78 | } 79 | }); 80 | }); 81 | 82 | router.get('/api/v1/instance', (req, res, next) => { 83 | let contact_account = { 84 | "locked": false, 85 | "bot": false, 86 | "discoverable": true, 87 | "group": false, 88 | "created_at": "2017-04-05T00:00:00.000Z", 89 | "note": "", 90 | "avatar": 'https://' + req.app.config.server.domain + '/' + req.app.config.default_avatar, 91 | "avatar_static": 'https://' + req.app.config.server.domain + '/' + req.app.config.default_avatar, 92 | "header": 'https://' + req.app.config.server.domain + '/' + req.app.config.default_header, 93 | "header_static": 'https://' + req.app.config.server.domain + '/' + req.app.config.default_header, 94 | "followers_count": 0, 95 | "following_count": 0, 96 | "statuses_count": 0, 97 | "last_status_at": "2022-12-12", 98 | "noindex": false, 99 | "emojis": [], 100 | "fields": [] 101 | }; 102 | 103 | req.app.db.user_getByName(req.app.config.server.adminuser).then((user) => { 104 | if (user === null) { 105 | console.log("ERROR! config.server.adminuser not valid"); 106 | } else { 107 | contact_account.id = user.userid.toString(); 108 | contact_account.username = user.username; 109 | contact_account.acct = user.username; 110 | contact_account.display_name = user.username; 111 | contact_account.url = 'https://' + req.app.config.server.domain + '/@' + user.username; 112 | } 113 | }).catch((err) => { 114 | res.status(500).send(); 115 | }).then(() => { 116 | let rsp = { 117 | "uri": req.app.config.server.domain, 118 | "domain": req.app.config.server.domain, 119 | "source_url": 'https://' + req.app.config.server.domain, 120 | "title": req.app.config.server.title, 121 | "short_description": req.app.config.server.short_description, 122 | "description": req.app.config.server.description, 123 | "email": req.app.config.server.email, 124 | "version": "4.0.2", 125 | "urls": { 126 | "streaming_api": "wss://" + req.app.config.server.domain 127 | }, 128 | "stats": { 129 | "user_count": 1, 130 | "status_count": 1, 131 | "domain_count": 1 132 | }, 133 | "thumbnail": 'https://' + req.app.config.server.domain + '/' + req.app.config.server.thumbnail, 134 | "languages": [ 135 | "en" 136 | ], 137 | "registrations": false, 138 | "approval_required": false, 139 | "invites_enabled": false, 140 | "configuration": { 141 | "accounts": { 142 | "max_featured_tags": 10 143 | }, 144 | "statuses": { 145 | "max_characters": 500, 146 | "max_media_attachments": 4, 147 | "characters_reserved_per_url": 23 148 | }, 149 | "media_attachments": { 150 | "supported_mime_types": req.app.config.supported_mime_types, 151 | "image_size_limit": 10485760, 152 | "image_matrix_limit": 16777216, 153 | "video_size_limit": 41943040, 154 | "video_frame_rate_limit": 60, 155 | "video_matrix_limit": 2304000 156 | }, 157 | "polls": { 158 | "max_options": 4, 159 | "max_characters_per_option": 50, 160 | "min_expiration": 300, 161 | "max_expiration": 2629746 162 | } 163 | }, 164 | "contact_account": contact_account, 165 | "rules": [] 166 | }; 167 | 168 | res.status(200).send(rsp); 169 | }); 170 | }); 171 | 172 | router.get('/api/v1/accounts/relationships', passport.authenticate('bearer'), (req, res, next) => { 173 | console.log("/api/v1/accounts/relationships query=", req.query); 174 | 175 | if (req.query.id) { 176 | let accountid = util.decodeSafeB64(req.query.id); 177 | console.log("id=", accountid); 178 | 179 | // FIXME, this should be done in db, not by pulling all data out 180 | let following = false; 181 | let followed = false; 182 | req.app.db.following_allForUser(req.user.userid).then((followingList) => { 183 | followingList.forEach((f) => { 184 | if (f.actor === accountid) { 185 | following = true; 186 | } 187 | }); 188 | 189 | // FIXME, this should be done in db, not by pulling all data out 190 | req.app.db.follower_allForUser(req.user.userid).then((followerList) => { 191 | followerList.forEach((f) => { 192 | if (f.actor === accountid) { 193 | followed = true; 194 | } 195 | }); 196 | 197 | res.status(200).send([ 198 | { 199 | "id": req.query.id, 200 | "following": following, 201 | "showing_reblogs": true, 202 | "notifying": false, 203 | "followed_by": followed, 204 | "blocking": false, 205 | "blocked_by": false, 206 | "muting": false, 207 | "muting_notifications": false, 208 | "requested": false, 209 | "domain_blocking": false, 210 | "endorsed": false 211 | } 212 | ]); 213 | }); 214 | }); 215 | } else { 216 | res.status(200).send([]); 217 | } 218 | 219 | }); 220 | 221 | // expects {userid:, username:} 222 | function accountFromUser(app, user, config) { 223 | return { 224 | "id": util.encodeSafeB64(util.userActor(app, user)), 225 | "username": user.username, 226 | "acct": user.username, 227 | "display_name": user.username, 228 | "locked": false, 229 | "bot": false, 230 | "discoverable": true, 231 | "group": false, 232 | "created_at": "1970-01-01T00:00:00.000Z", 233 | "note": "

Note TBD

", 234 | "url": "https://" + config.server.domain + "/@" + user.username, 235 | "avatar": 'https://' + config.server.domain + '/' + config.default_avatar, 236 | "avatar_static": 'https://' + config.server.domain + '/' + config.default_avatar, 237 | "header": 'https://' + config.server.domain + '/' + config.default_header, 238 | "header_static": 'https://' + config.server.domain + '/' + config.default_header, 239 | "followers_count": 0, 240 | "following_count": 0, 241 | "statuses_count": 0, 242 | "last_status_at": "2022-12-12", 243 | "noindex": true, 244 | "source": { 245 | "privacy": "public", 246 | "sensitive": false, 247 | "language": null, 248 | "note": "", 249 | "fields": [], 250 | "follow_requests_count": 0 251 | }, 252 | "emojis": [], 253 | "fields": [], 254 | "role": { 255 | "id": "-99", 256 | "name": "", 257 | "permissions": "0", 258 | "color": "", 259 | "highlighted": false 260 | } 261 | }; 262 | } 263 | 264 | router.get('/api/v1/accounts/verify_credentials', passport.authenticate('bearer'), (req, res, next) => { 265 | console.log("/api/v1/accounts/verify_credentials"); 266 | 267 | req.app.cmd.getActor(req.app, req.user, util.userActor(req.app, req.user)).then((actor) => { 268 | let account = util.actorToMastodonAccount(req.app, actor); 269 | 270 | // Extend to a CredentialAccount 271 | account.source = { 272 | "privacy": "public", 273 | "sensitive": false, 274 | "language": "", 275 | "note": "", 276 | "fields": [], 277 | "follow_requests_count": 0 278 | }; 279 | 280 | res.status(200).send(account); 281 | }).catch((err) => { 282 | console.log("verify_credentials error", err); 283 | res.status(500).send(err); 284 | }); 285 | }); 286 | 287 | router.get('/api/v1/accounts/lookup', (req, res, next) => { 288 | console.log("PUBLIC /api/v1/accounts/lookup", req.query); 289 | req.app.db.user_getByName(req.query.acct).then((user) => { 290 | if (user) { 291 | console.log("LOOKUP found", user.username); 292 | res.status(200).send(accountFromUser(req.app, user, req.app.config)); 293 | } else { 294 | res.status(404).send(); 295 | } 296 | }).catch((err) => { 297 | res.status(500).send(err); 298 | }); 299 | }); 300 | 301 | router.get('/api/v1/accounts/:accountid/statuses', (req, res, next) => { 302 | if (req.get('Authorization')) { 303 | next(); // next match, authenticated version 304 | } else { 305 | console.log("PUBLIC /api/v1/accounts/" + req.params.accountid + "/statuses"); 306 | req.app.db.user_get(parseInt(req.params.accountid)).then((user) => { 307 | if (user === null) { 308 | res.status(404).send(); 309 | } else { 310 | res.status(200).send([]); 311 | } 312 | }).catch((err) => { 313 | res.status(500).send(); 314 | }); 315 | } 316 | }); 317 | 318 | router.get('/api/v1/accounts/:accountid/statuses', passport.authenticate('bearer'), (req, res, next) => { 319 | console.log("PRIVATE /api/v1/accounts/" + req.params.accountid + "/statuses"); 320 | 321 | res.status(200).send([]); 322 | 323 | // FIXME, this is good for mastodon 324 | // FIXME need fallback to db knowledge 325 | /* 326 | req.app.db.user_get(parseInt(req.params.accountid)).then((user) => { 327 | if (user === null) { // not from this server 328 | let accountUrl = util.decodeSafeB64(req.params.accountid); 329 | if (!accountUrl.startsWith('http')) { // also not an encoded URL 330 | res.status(404).send(); 331 | } else { 332 | // get username from URL 333 | let urlObj = urlparser.parse(accountUrl, true); 334 | let remoteUser = path.basename(urlObj.pathname); 335 | let remoteServerUrl = urlObj.protocol + "//" + urlObj.host; 336 | let lookupUrl = remoteServerUrl + '/api/v1/accounts/lookup?acct=' + remoteUser; 337 | 338 | // ask remote server for account info for user 339 | fetch(lookupUrl).then((res) => { 340 | return res.json(); 341 | }).then((account) => { 342 | // ask remote server for their statuses 343 | fetch(remoteServerUrl + '/api/v1/accounts/' + account.id + '/statuses').then((res) => { 344 | return res.json(); 345 | }).then((statuses) => { 346 | //console.log(statuses); 347 | // pass back to caller 348 | res.status(200).send(statuses); // FIXME error handling, pipe? 349 | }); 350 | }); 351 | } 352 | } else { 353 | res.status(200).send([]); 354 | } 355 | }).catch((err) => { 356 | res.status(500).send(); 357 | }); 358 | */ 359 | }); 360 | 361 | router.delete('/api/v1/statuses/:statusid', passport.authenticate('bearer'), (req, res, next) => { 362 | let id = util.decodeSafeB64(req.params.statusid); 363 | console.log("delete /api/v1/statuses", id); 364 | 365 | // FIXME FIXME FIXME validate that the status being deleted belongs to user 366 | 367 | req.app.cmd.deleteNote(req.app, req.user.userid, id).then(() => { 368 | //res.status(200).send({}); // FIXME response? 369 | return res.json({}); // FIXME should send back the Status 370 | }); 371 | 372 | }); 373 | 374 | function recurseAncestors(app, user, ancestors, todo, seen) { 375 | //console.log("RECANC", todo); 376 | if (todo.length == 0) { 377 | return Promise.resolve(); 378 | } else { 379 | let url = todo.shift(); 380 | if (seen.includes(url)) { 381 | return Promise.reject('Circular thread! '+ url); 382 | } 383 | seen.push(url); 384 | return util.getObject(app, url).then((msg) => { 385 | //console.log(msg); 386 | // fetched a Note, but util.msgToStatus is expecting a Create with embedded Note 387 | return util.msgToStatus(app, user, { 388 | id: url, 389 | actor: msg.attributedTo, 390 | object: msg, 391 | type: "Create", 392 | published: msg.published 393 | }); 394 | }).then((st) => { 395 | ancestors.push(st); 396 | if (st.in_reply_to_id) { 397 | todo.push(util.decodeSafeB64(st.in_reply_to_id)); 398 | //console.log("ANCESTOR PUSH", util.decodeSafeB64(st.in_reply_to_id)); 399 | } 400 | return recurseAncestors(app, user, ancestors, todo, seen); 401 | }); 402 | } 403 | } 404 | 405 | function collectionToStatuses(app, user, descendants, coll) { 406 | if (coll.first) { 407 | if (coll.first.type === 'CollectionPage') { 408 | let collUrl = coll.first.next; 409 | let httpStatus; 410 | console.log("Fetching ", collUrl); 411 | return fetch(collUrl, { 412 | method: 'GET', 413 | headers: { 'Accept': 'application/activity+json' } 414 | }).then((res) => { 415 | httpStatus = res.status; 416 | if (res.status >= 200 && res.status < 300) { 417 | return res.text(); 418 | } else { 419 | return Promise.reject(`${collUrl} http ${httpStatus}`); 420 | } 421 | }).then((text) => { 422 | return Promise.resolve(JSON.parse(text)); 423 | }).then((collpage) => { 424 | //console.log("COLLPAGE", collpage); 425 | return Promise.each(collpage.items, function(item, index, arrayLength) { 426 | if (typeof item === 'object') { 427 | let msg = item; 428 | console.log("OBJ", item.id); 429 | if (msg.type === 'Note') { 430 | // fetched a Note, but util.msgToStatus is expecting a Create with embedded Note 431 | return util.msgToStatus(app, user, { 432 | id: util.encodeSafeB64(item.id), 433 | actor: util.encodeSafeB64(msg.attributedTo), 434 | object: msg, 435 | type: "Create", 436 | published: msg.published 437 | }).then((st) => { 438 | //st.id = util.decodeSafeB64(st.id); // HACK 439 | console.log("DESC STATUS", st.id); 440 | descendants.push(st); 441 | return Promise.resolve(); 442 | }); 443 | } else { 444 | return Promise.resolve(); 445 | } 446 | } else { 447 | console.log("ID", item); 448 | console.log("Fetching ", item); 449 | let httpStatus; 450 | return fetch(item, { 451 | method: 'GET', 452 | headers: { 'Accept': 'application/activity+json' } 453 | }).then((res) => { 454 | httpStatus = res.status; 455 | if (res.status >= 200 && res.status < 300) { 456 | return res.text(); 457 | } else { 458 | console.log(`${item} http ${httpStatus}`); 459 | return null; // allow failures 460 | } 461 | }).then((text) => { 462 | if (text) { 463 | return Promise.resolve(JSON.parse(text)); 464 | } else { 465 | return Promise.resolve(null); 466 | } 467 | }).then((msg) => { 468 | console.log(msg); 469 | if (msg) { 470 | return util.msgToStatus(app, user, { 471 | id: util.encodeSafeB64(item), 472 | actor: util.encodeSafeB64(msg.attributedTo), 473 | object: msg, 474 | type: "Create", 475 | published: msg.published 476 | }).then((st) => { 477 | console.log("DESC STATUS", st.id); 478 | descendants.push(st); 479 | return Promise.resolve(); 480 | }); 481 | } else { 482 | return Promise.resolve(); 483 | } 484 | }).catch((err) => { 485 | console.log(`Fetching ${item} failed`); 486 | }); 487 | } 488 | }); 489 | }).then(() => { 490 | console.log("PROCESSED PAGE"); 491 | }); 492 | } 493 | } else { 494 | return Promise.resolve(); // FIXME not first 495 | } 496 | } 497 | 498 | function recurseDescendants(app, user, descendants, todo) { 499 | if (todo.length == 0) { 500 | return Promise.resolve(); 501 | } else { 502 | let url = todo.shift(); 503 | return util.getObject(app, url).then((msg) => { 504 | if (msg.type !== 'Note') { 505 | console.log("RECURSEDESC expected Note!"); 506 | return Promise.resolve(); 507 | } 508 | if (msg.replies) { 509 | console.log("RECURSEDESC", msg.replies); 510 | if (msg.replies.type === 'Collection') { 511 | return collectionToStatuses(app, user, descendants, msg.replies); 512 | } else { 513 | return Promise.resolve(); 514 | } 515 | } else { 516 | return Promise.resolve(); 517 | } 518 | }); 519 | } 520 | } 521 | 522 | router.get('/api/v1/statuses/:statusid/context', passport.authenticate('bearer'), (req, res, next) => { 523 | let statusUrl = util.decodeSafeB64(req.params.statusid); 524 | console.log("/api/v1/statuses/", statusUrl, "context"); 525 | 526 | // FIXME FIXME FIXME validate that the status being queried belongs to user or is public 527 | 528 | let ctx = { 529 | ancestors: [], 530 | descendants: [] 531 | }; 532 | 533 | let ancestorsTodo = [statusUrl]; 534 | let descendantsTodo = [statusUrl]; 535 | let ancestorsSeen = []; 536 | recurseDescendants(req.app, req.user, ctx.descendants, descendantsTodo).then(() => { 537 | recurseAncestors(req.app, req.user, ctx.ancestors, ancestorsTodo, ancestorsSeen).then(() => { 538 | ctx.ancestors.reverse(); 539 | console.log("context ancestors:", ctx.ancestors.map((e) => {return e.uri})); 540 | console.log("context descendants:", ctx.descendants.map((e) => {return e.uri})); 541 | res.status(200).send(ctx); 542 | }).catch((err) => { 543 | res.status(500).send(err); 544 | }); 545 | }); 546 | }); 547 | 548 | 549 | 550 | router.get('/api/v1/statuses/:statusid', passport.authenticate('bearer'), (req, res, next) => { 551 | let statusUrl = util.decodeSafeB64(req.params.statusid); 552 | console.log("/api/v1/statuses/", statusUrl); 553 | 554 | // FIXME FIXME FIXME validate that the status being queried belongs to user or is public 555 | util.getObject(req.app, statusUrl).then((msg) => { 556 | // fetched a Note, but util.msgToStatus is expecting a Create with embedded Note 557 | return util.msgToStatus(req.app, req.user, { 558 | id: statusUrl, 559 | actor: msg.attributedTo, 560 | object: msg, 561 | type: "Create", 562 | published: msg.published 563 | }); 564 | }).then((st) => { 565 | res.status(200).send(st); 566 | }).catch((err) => { 567 | res.status(500).send(err); 568 | }); 569 | }); 570 | 571 | 572 | router.post('/api/v1/statuses/:statusid/reblog', passport.authenticate('bearer'), (req, res, next) => { 573 | let statusid = util.decodeSafeB64(req.params.statusid); 574 | console.log("REBLOG ", statusid); 575 | 576 | req.app.db.message_get(statusid).then((msg) => { 577 | console.log(msg); 578 | return Promise.resolve(msg); 579 | }).then((msg) => { 580 | return req.app.cmd.sendAnnounce(req.app, req.user.userid, statusid, msg.message.object.attributedTo).then(() => { 581 | res.status(200).send({}); // FIXME should return Status 582 | }); 583 | }).catch((err) => { 584 | console.log("ERR", err); 585 | res.status(500).send(err); 586 | }); 587 | }); 588 | 589 | router.post('/api/v1/statuses/:statusid/unreblog', passport.authenticate('bearer'), (req, res, next) => { 590 | let statusid = util.decodeSafeB64(req.params.statusid); 591 | console.log("UNREBLOG ", statusid); 592 | 593 | req.app.db.message_getByObjectId(statusid).then((msg) => { 594 | return Promise.resolve(msg); 595 | }).then((msg) => { 596 | return req.app.cmd.sendUnannounce(req.app, req.user.userid, statusid, msg.message.object.attributedTo).then(() => { 597 | res.status(200).send({}); // FIXME should return Status 598 | }); 599 | }).catch((err) => { 600 | console.log("ERR", err); 601 | res.status(500).send(err); 602 | }); 603 | }); 604 | 605 | router.post('/api/v1/statuses/:safestatusid/unfavourite', passport.authenticate('bearer'), (req, res, next) => { 606 | let statusid = util.decodeSafeB64(req.params.safestatusid); 607 | console.log("/api/v1/statuses/../unfavourite" + statusid); 608 | 609 | req.app.db.message_get(statusid).then((msg) => { 610 | console.log(msg); 611 | return Promise.resolve(msg); 612 | }).then((msg) => { 613 | return req.app.cmd.sendUnlike(req.app, req.user.userid, statusid, msg.message.object.attributedTo).then(() => { 614 | res.status(200).send({}); // FIXME should return Status 615 | }); 616 | }).catch((err) => { 617 | console.log("ERR", err); 618 | res.status(500).send(err); 619 | }); 620 | }); 621 | 622 | router.post('/api/v1/statuses/:safestatusid/favourite', passport.authenticate('bearer'), (req, res, next) => { 623 | let statusid = util.decodeSafeB64(req.params.safestatusid); 624 | 625 | console.log("/api/v1/statuses/../favourite", statusid); 626 | 627 | // If the url is one of ours, get from db would end in /object 628 | if (statusid.startsWith(`${util.getMsgPrefix(req.app)}`)) { 629 | let [id, subtype] = statusid.replace(`${util.getMsgPrefix(req.app)}`, '').split('/'); 630 | statusid = `${util.getMsgPrefix(req.app)}${id}`; 631 | } 632 | 633 | req.app.db.message_get(statusid).then((msg) => { 634 | return Promise.resolve(msg); 635 | }).then((msg) => { 636 | return req.app.cmd.sendLike(req.app, req.user.userid, statusid, msg.message.object.attributedTo).then(() => { 637 | res.status(200).send({}); // FIXME should return Status 638 | }); 639 | }).catch((err) => { 640 | console.log("ERR", err); 641 | res.status(500).send(err); 642 | }); 643 | }); 644 | 645 | router.post('/api/v1/accounts/:safeaccountid/unfollow', passport.authenticate('bearer'), (req, res, next) => { 646 | let accountid = util.decodeSafeB64(req.params.safeaccountid); 647 | console.log("/api/v1/accounts/../unfollow" + accountid); 648 | 649 | // FIXME repetition unfollow/follow 650 | // need a general function to get the Relationship 651 | 652 | // FIXME, this should be done in db, not by pulling all data out 653 | let followed = false; 654 | req.app.db.follower_allForUser(req.user.userid).then((followerList) => { 655 | followerList.forEach((f) => { 656 | if (f.actor === accountid) { 657 | followed = true; 658 | } 659 | }); 660 | 661 | req.app.cmd.sendUnfollow(req.app, req.user.userid, accountid).then(() => { 662 | res.status(200).send({ 663 | "id": accountid, 664 | "following": false, 665 | "showing_reblogs": true, 666 | "notifying": false, 667 | "followed_by": followed, 668 | "blocking": false, 669 | "blocked_by": false, 670 | "muting": false, 671 | "muting_notifications": false, 672 | "requested": false, 673 | "domain_blocking": false, 674 | "endorsed": false 675 | }); 676 | }).catch((err) => { 677 | console.log("UNFOLLOW ERR", err); 678 | res.status(400).send(err); 679 | }); 680 | }); 681 | }); 682 | 683 | router.post('/api/v1/accounts/:safeaccountid/follow', passport.authenticate('bearer'), (req, res, next) => { 684 | let accountid = util.decodeSafeB64(req.params.safeaccountid); 685 | console.log("/api/v1/accounts/../follow" + accountid); 686 | 687 | // FIXME, this should be done in db, not by pulling all data out 688 | let followed = false; 689 | req.app.db.follower_allForUser(req.user.userid).then((followerList) => { 690 | followerList.forEach((f) => { 691 | if (f.actor === accountid) { 692 | followed = true; 693 | } 694 | }); 695 | 696 | req.app.cmd.sendFollow(req.app, req.user.userid, accountid).then(() => { 697 | res.status(200).send({ 698 | "id": accountid, 699 | "following": true, 700 | "showing_reblogs": true, 701 | "notifying": false, 702 | "followed_by": followed, 703 | "blocking": false, 704 | "blocked_by": false, 705 | "muting": false, 706 | "muting_notifications": false, 707 | "requested": false, 708 | "domain_blocking": false, 709 | "endorsed": false 710 | }); 711 | }).catch((err) => { 712 | console.log("UNFOLLOW ERR", err); 713 | res.status(400).send(err); 714 | }); 715 | }); 716 | }); 717 | 718 | 719 | 720 | router.get('/api/v1/accounts/:safeaccountid', (req, res, next) => { 721 | let accountid = util.decodeSafeB64(req.params.safeaccountid); 722 | console.log("/api/v1/accounts/" + req.params.safeaccountid + accountid); 723 | 724 | return req.app.cmd.getActor(req.app, req.user, accountid).then((actor) => { 725 | res.status(200).send(util.actorToMastodonAccount(req.app, actor)); 726 | }).catch((err) => { 727 | res.status(500).send(err); 728 | }); 729 | }); 730 | 731 | router.get('/api/v1/trends', (req, res, next) => { 732 | console.log("/api/v1/trends/statuses"); 733 | res.status(200).send([]); 734 | }); 735 | 736 | router.get('/api/v1/trends/statuses', (req, res, next) => { 737 | console.log("/api/v1/trends/statuses"); 738 | res.status(200).send([]); 739 | }); 740 | 741 | router.get('/api/v1/trends/links', (req, res, next) => { 742 | console.log("/api/v1/trends/links"); 743 | res.status(200).send([]); 744 | }); 745 | 746 | router.get('/api/v1/custom_emojis', (req, res, next) => { 747 | console.log("/api/v1/custom_emojis"); 748 | res.status(200).send([]); 749 | }); 750 | 751 | router.get('/api/v1/lists', passport.authenticate('bearer'), (req, res, next) => { 752 | console.log("/api/v1/lists"); 753 | res.status(200).send([]); 754 | }); 755 | 756 | router.get('/api/v1/filters', passport.authenticate('bearer'), (req, res, next) => { 757 | console.log("/api/v1/filters"); 758 | res.status(200).send([]); 759 | }); 760 | 761 | router.get('/api/v1/mutes', passport.authenticate('bearer'), (req, res, next) => { 762 | console.log("/api/v1/mutes"); 763 | res.status(200).send([]); 764 | }); 765 | 766 | router.get('/api/v1/blocks', passport.authenticate('bearer'), (req, res, next) => { 767 | console.log("/api/v1/blocks"); 768 | res.status(200).send([]); 769 | }); 770 | 771 | router.get('/api/v1/notifications', passport.authenticate('bearer'), (req, res, next) => { 772 | console.log("/api/v1/notifications"); 773 | res.status(200).send([]); 774 | }); 775 | 776 | router.get('/api/v1/markers', passport.authenticate('bearer'), (req, res, next) => { 777 | console.log("/api/v1/markers"); 778 | res.status(200).send([]); 779 | }); 780 | 781 | router.get('/api/v1/conversations', passport.authenticate('bearer'), (req, res, next) => { 782 | console.log("/api/v1/conversations"); 783 | res.status(200).send([]); 784 | }); 785 | 786 | router.get('/api/v1/announcements', passport.authenticate('bearer'), (req, res, next) => { 787 | console.log("/api/v1/announcements"); 788 | res.status(200).send([]); 789 | }); 790 | 791 | router.get('/api/v1/preferences', passport.authenticate('bearer'), (req, res, next) => { 792 | console.log("/api/v1/preferences"); 793 | res.status(200).send({ 794 | "posting:default:visibility": "public", 795 | "posting:default:sensitive": false, 796 | "posting:default:language": null, 797 | "reading:expand:media": "default", 798 | "reading:expand:spoilers": false 799 | }); 800 | }); 801 | 802 | function mediaIdsToAttachment(app, media_ids) { 803 | let attachment = []; 804 | return Promise.each(media_ids, function(media_id, index, arrayLength) { 805 | return app.db.media_get(media_id).then((media) => { 806 | let meta = JSON.parse(media.meta); 807 | let att = { 808 | type: 'Document', 809 | mediaType: media.type, 810 | url: `https://${app.config.server.domain}/media/${media.guid}`, 811 | name: media.description, 812 | blurhash: media.blurhash, 813 | focalPoint: meta.focus, 814 | width: meta.original.width, 815 | height: meta.original.height 816 | }; 817 | console.log("ATT", att); 818 | attachment.push(att); 819 | return Promise.resolve(); 820 | }); 821 | }).then(() => { 822 | return Promise.resolve(attachment); 823 | }); 824 | } 825 | 826 | router.post('/api/v1/statuses', passport.authenticate('bearer'), (req, res, next) => { 827 | console.log('POST /api/v1/statuses ', req.body); 828 | 829 | let in_reply_to_id = null; 830 | if (req.body.in_reply_to_id) { 831 | in_reply_to_id = util.decodeSafeB64(req.body.in_reply_to_id); 832 | } 833 | 834 | let media_ids = []; 835 | if (req.body.media_ids) { 836 | media_ids = req.body.media_ids; 837 | } 838 | 839 | let spoiler_text = null; 840 | if (req.body.spoiler_text) { 841 | spoiler_text = req.body.spoiler_text; 842 | } 843 | 844 | let sensitive = false; 845 | if (req.body.sensitive) { 846 | sensitive = req.body.sensitive; 847 | } 848 | 849 | mediaIdsToAttachment(req.app, media_ids).then((attachment) => { 850 | return req.app.cmd.sendNote(req.app, req.user.userid, req.body.status, in_reply_to_id, attachment, spoiler_text, sensitive).then((msg) => { 851 | return util.msgToStatus(req.app, req.user, msg).then((st) => { 852 | return new Promise((resolve, reject) => { 853 | res.on('close', () => { 854 | resolve(); 855 | }); 856 | res.status(200).send(st); 857 | }).then(() => { 858 | // send event to update live timeline 859 | return req.app.ev.send('update', req.user.userid, msg.object.id, util.userActor(req.app, req.user)); 860 | }); 861 | }); 862 | }); 863 | }).catch((err) => { 864 | console.log("send note failed", err); 865 | res.status(500).send(err); 866 | }); 867 | }); 868 | 869 | router.get('/api/v1/timelines/public', (req, res, next) => { 870 | console.log("/api/v1/timelines/public"); 871 | res.status(200).send([]); 872 | }); 873 | 874 | 875 | router.get('/api/v1/timelines/home', passport.authenticate('bearer'), (req, res, next) => { 876 | console.log("/api/v1/timelines/home query=", req.query); 877 | 878 | // no caching 879 | res.set({ 880 | "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate", 881 | "Pragma": "no-cache", 882 | "Expires": "0", 883 | "Surrogate-Control": "no-store" 884 | }); 885 | 886 | req.app.db.timeline(req.user.userid, util.userActor(req.app, req.user)).then((messageDatas) => { 887 | let statuses = []; 888 | console.log("timeline len=", messageDatas.length); 889 | //messageDatas.forEach((m) => { 890 | // console.log(m.guid, m.actor, m.created_at, m.message.type, m.message.object.content); 891 | //}); 892 | 893 | // FIXME, this needs to be done in db 894 | if (req.query.max_id) { 895 | let accept = false; 896 | messageDatas = messageDatas.filter((m) => { 897 | //console.log(m.message.object.id, req.query.max_id); 898 | if (util.encodeSafeB64(m.message.object.id) === req.query.max_id) { 899 | accept = true; 900 | return false; // not this one, but do accept those after it 901 | } 902 | return accept; 903 | }); 904 | } 905 | if (req.query.limit) { 906 | messageDatas = messageDatas.slice(0, req.query.limit); 907 | } 908 | 909 | Promise.each(messageDatas, function(messageData, index, arrayLength) { 910 | return util.msgToStatus(req.app, req.user, messageData.message).then((st) => { 911 | if (st) { // util.msgToStatus may return null to filter message out 912 | statuses.push(st); 913 | } 914 | return Promise.resolve(); 915 | }); 916 | }).then(() => { 917 | res.status(200).send(statuses); 918 | }).catch((err) => { 919 | console.log("timeline err", err); 920 | res.status(500).send(err); 921 | }) 922 | }).catch((err) => { 923 | res.status(500).send(err); 924 | }); 925 | }); 926 | 927 | // resolves to filename of tmp file already on disk 928 | function parseUploadForm(req) { 929 | return new Promise((resolve, reject) => { 930 | const inform = formidable({ multiples: true }); 931 | inform.parse(req, (err, fields, files) => { 932 | if (err) { 933 | reject('bad formdata'); 934 | } else { 935 | console.log("FIELDS", fields); 936 | if (files.file) { 937 | resolve({ 938 | path: files.file.filepath, 939 | mimetype: files.file.mimetype, 940 | size: files.file.size 941 | }); 942 | } else { 943 | reject('no file'); 944 | } 945 | } 946 | }); 947 | }); 948 | } 949 | 950 | function mediaToAttachment(app, media) { 951 | return { 952 | "id": media.guid, 953 | "type": media.type.split('/')[0], 954 | "url": `https://${app.config.server.domain}/media/${media.guid}`, 955 | "preview_url": `https://${app.config.server.domain}/media/${media.guid}/preview`, 956 | "remote_url": null, 957 | "text_url": `https://${app.config.server.domain}/media/${media.guid}/text`, 958 | "meta": JSON.parse(media.meta), 959 | "description": media.description, 960 | "blurhash": media.blurhash 961 | }; 962 | } 963 | 964 | router.post('/api/v2/media', passport.authenticate('bearer'), (req, res, next) => { 965 | console.log('/api/v2/media'); 966 | let fileinfo; 967 | let data; 968 | let guid; 969 | let blurhash; 970 | let dimensions = {width: 640, height: 480}; // some defaults for unknowns 971 | let statusCode = 500; 972 | 973 | parseUploadForm(req).then((_fileinfo) => { 974 | fileinfo = _fileinfo; 975 | console.log("UPLOAD path", fileinfo.path, fileinfo.mimetype); 976 | // validate against list of allowed mimetypes 977 | if (!req.app.config.supported_mime_types.includes(fileinfo.mimetype)) { 978 | statusCode = 400; 979 | return Promise.reject('unsupported file format'); 980 | } else { 981 | return util.readFilePromise(fileinfo.path); 982 | } 983 | }).then((_data) => { 984 | data = _data; 985 | // work out dimensions while we have a file on disk 986 | if (fileinfo.mimetype.split('/')[0] === 'image') { 987 | dimensions = imageSize(fileinfo.path) 988 | console.log("DIMENSIONS", dimensions); 989 | } 990 | console.log("UPLOAD SIZE", fileinfo.size); 991 | return util.imageFileToBlurhash(fileinfo.path); 992 | }).then((_blurhash) => { 993 | blurhash = _blurhash; 994 | return util.unlinkFilePromise(fileinfo.path); 995 | }).then(() => { 996 | return util.generateGuid(); 997 | }).then((_guid) => { 998 | guid = _guid; 999 | // FIXME should generate smaller preview 1000 | let defaultMeta = { 1001 | "focus": "0.0,0.0", 1002 | "original": { 1003 | "width": dimensions.width, 1004 | "height": dimensions.height, 1005 | "size": `${dimensions.width}x${dimensions.height}`, 1006 | "aspect": dimensions.width / dimensions.height 1007 | }, 1008 | "small": { 1009 | "width": dimensions.width, 1010 | "height": dimensions.height, 1011 | "size": `${dimensions.width}x${dimensions.height}`, 1012 | "aspect": dimensions.width / dimensions.height 1013 | } 1014 | }; 1015 | 1016 | return req.app.db.media_add(guid, req.user.userid, data, data, fileinfo.mimetype, blurhash, "", JSON.stringify(defaultMeta)); // default to a real blurhash 1017 | }).then((media) => { 1018 | res.status(200).send(mediaToAttachment(req.app, media)); 1019 | }).catch((err) => { 1020 | console.log("Upload err", err); 1021 | // something went wrong, make sure file is deleted 1022 | if (fileinfo.path) { 1023 | util.unlinkFilePromise(path).then(() => { 1024 | res.status(statusCode).send({ 1025 | error: 'upload error', 1026 | error_description: err 1027 | }); 1028 | }).catch((err2) => { 1029 | res.status(statusCode).send({ 1030 | error: 'upload error', 1031 | error_description: err 1032 | }); 1033 | }); 1034 | } 1035 | }); 1036 | }); 1037 | 1038 | router.get('/media/:guid/preview', /*passport.authenticate('bearer'),*/ (req, res, next) => { 1039 | console.log('GET media preview', req.params.guid); 1040 | // FIXME FIXME FIXME, permissions on media, not everything should be public 1041 | req.app.db.media_get(req.params.guid).then((media) => { 1042 | if (!media) { 1043 | res.status(404).send(); 1044 | } else { 1045 | // send data 1046 | res.set('Content-Type', media.type); 1047 | res.status(200).send(media.preview); 1048 | } 1049 | }).catch((err) => { 1050 | res.status(500).send(err); 1051 | }); 1052 | }); 1053 | 1054 | router.get('/media/:guid', /*passport.authenticate('bearer'),*/ (req, res, next) => { 1055 | console.log('GET media ', req.params.guid); 1056 | // FIXME FIXME FIXME, permissions on media, not everything should be public 1057 | req.app.db.media_get(req.params.guid).then((media) => { 1058 | if (!media) { 1059 | res.status(404).send(); 1060 | } else { 1061 | // send data 1062 | res.set('Content-Type', media.type); 1063 | res.status(200).send(media.file); 1064 | } 1065 | }).catch((err) => { 1066 | res.status(500).send(err); 1067 | }); 1068 | }); 1069 | 1070 | router.put('/api/v2/media/:guid', passport.authenticate('bearer'), (req, res, next) => { 1071 | console.log('PUT media ', req.params.guid); 1072 | // FIXME FIXME FIXME, permissions on media, not everything should be public 1073 | req.app.db.media_get(req.params.guid).then((media) => { 1074 | if (!media) { 1075 | res.status(404).send(); 1076 | } else { 1077 | console.log("PRE-UPDATE", media); 1078 | media.meta = JSON.parse(media.meta); // expand to JSON 1079 | if (req.body.description) { 1080 | media.description = req.body.description; 1081 | } 1082 | if (req.body.focus) { 1083 | media.meta.focus = req.body.focus; 1084 | } 1085 | // FIXME doc says thumbnail can be uploaded too 1086 | media.meta = JSON.stringify(media.meta); // back to string 1087 | console.log("UPSERTING", media); 1088 | return req.app.db.media_add(media.guid, req.user.userid, media.file, media.preview, media.type, media.blurhash, media.description, media.meta).then((media) => { 1089 | res.status(200).send(mediaToAttachment(req.app, media)); 1090 | }).catch((err) => { 1091 | console.log('PUT media failed', err); 1092 | res.status(500).send(err); 1093 | }); 1094 | } 1095 | }).catch((err) => { 1096 | res.status(500).send(err); 1097 | }); 1098 | }); 1099 | 1100 | module.exports = router; 1101 | 1102 | -------------------------------------------------------------------------------- /cubiti-server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const PORT = 8002; 4 | 5 | // workaround for node bug, https://github.com/nodejs/node/issues/40702 6 | const dns = require('dns'); 7 | dns.setDefaultResultOrder('ipv4first'); 8 | 9 | var express = require('express'), 10 | app = express(); 11 | var bodyParser = require('body-parser'); 12 | const passport = require('passport'); 13 | const BearerStrategy = require('passport-bearer-strategy'); 14 | const sessions = require('express-session'); 15 | const LocalStrategy = require('passport-local'); 16 | var cors = require('cors'); 17 | var websock = require('./websock.js'); 18 | 19 | var CONFIG = require('./config.js'); 20 | var config = new CONFIG('/data/config.json'); 21 | 22 | var DB = require('./db.js'); 23 | var SYSEVENTS = require('./sysevents.js'); 24 | var db = new DB(); 25 | var ev = new SYSEVENTS(); 26 | 27 | ev.open().then(() => { 28 | return db.open('/data/my.db'); 29 | }).then(() => { 30 | var CMD = require('./cmd.js'); 31 | var cmd = new CMD(db); 32 | 33 | app.db = db; 34 | app.config = config; 35 | app.cmd = cmd; 36 | app.ev = ev; 37 | 38 | var CLI = require('./cli.js'); 39 | var cli = new CLI(app); 40 | cli.run(); 41 | 42 | app.set('view engine', 'ejs'); 43 | 44 | app.use(cors()); 45 | 46 | app.use(sessions({ 47 | secret: "ba990224bd431035c07bd0c4c3974d7c", 48 | saveUninitialized: false, 49 | resave: true, 50 | cookie: { 51 | sameSite: 'none', 52 | maxAge: 1000 * 60 * 60 * 24, 53 | secure: false // FIXME true for HTTPS only 54 | } 55 | })); 56 | 57 | app.use(bodyParser.json()); 58 | app.use(bodyParser.json({type: 'application/activity+json'})); 59 | app.use(bodyParser.urlencoded({ extended: true })); 60 | app.use(passport.initialize()); 61 | app.use(passport.session()); 62 | 63 | passport.use(new LocalStrategy((username, password, done) => { 64 | // console.log("LOCAL", username, password); 65 | db.user_validatePassword({username: username, password: password}).then((user) => { 66 | console.log("USER", user); 67 | if (user) { 68 | done(null, user); 69 | } else { 70 | done(null, false); 71 | } 72 | }).catch((err) => { 73 | done(err); 74 | }); 75 | })); 76 | 77 | passport.use(new BearerStrategy({passReqToCallback: true}, (req, token, done) => { 78 | //console.log("BEARER", token); 79 | db.user_validateToken(token).then((user) => { 80 | if (user !== null) { 81 | done(null, user, {scope: 'all' }); // FIXME, what's scope about, this arg? 82 | } else { 83 | done(null, false); 84 | } 85 | }).catch((err) => { 86 | console.log(err); 87 | return done(err); 88 | }); 89 | })); 90 | 91 | passport.serializeUser(function(user, cb) { 92 | // console.log("passport.serializeUser", user); 93 | return cb(null, user); 94 | }); 95 | 96 | passport.deserializeUser(function(user, cb) { 97 | // console.log("passport.deserializeUser", user); 98 | cb(null, user); 99 | }); 100 | 101 | // Expose static assets from public dir 102 | app.use(express.static('public')); 103 | 104 | // Attach routes 105 | app.use('', require('./routes/mastodon')); 106 | app.use('', require('./routes/activitypub')); 107 | }).then(() => { 108 | // start server 109 | app.listen(PORT, () => { 110 | console.log('Listening on port ' + PORT); 111 | websock.start(app); 112 | return Promise.resolve(); 113 | }); 114 | }).catch((err) => { 115 | console.log(err); 116 | process.exit(1); 117 | }); 118 | 119 | 120 | // Allow ctrl-c to kill 121 | process.on('SIGINT', function() { 122 | process.exit(); 123 | }); 124 | 125 | process.on('unhandledRejection', (reason, p) => { 126 | console.error(reason, 'Unhandled Rejection at Promise', p); 127 | }).on('uncaughtException', err => { 128 | console.error(err, 'Uncaught Exception thrown'); 129 | }); 130 | 131 | module.exports = { 132 | app: app, 133 | port: PORT, 134 | }; 135 | 136 | 137 | -------------------------------------------------------------------------------- /cubiti-server/sysevents.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let events = require('events'); 4 | let redis = require('redis'); 5 | 6 | var CONFIG = require('./config.js'); 7 | var config = new CONFIG('/data/config.json'); 8 | 9 | var SYSEVENTS = function () { 10 | this.client = null; 11 | this.channel = null; 12 | this.sub = null; 13 | }; 14 | 15 | SYSEVENTS.prototype.open = async function () { 16 | if (!config.redis) { 17 | console.log('configure redis in config.json, required for live timeline updates'); 18 | return Promise.resolve(); 19 | } 20 | this.client = redis.createClient({ 21 | url: config.redis 22 | }); 23 | 24 | return this.client.connect(); 25 | }; 26 | 27 | SYSEVENTS.prototype.close = async function () { 28 | console.log("sysevent closing"); 29 | if (!config.redis) { 30 | return; 31 | } 32 | 33 | if (this.sub) { 34 | this.sub.disconnect(); 35 | } 36 | this.client.disconnect().then(() => { 37 | console.log("sysevent closed"); 38 | }); 39 | }; 40 | 41 | SYSEVENTS.prototype.send = async function (type, userid, object, actor) { 42 | console.log(`sysevents.send ${type} ${userid} ${object} ${actor}`); 43 | if (!config.redis) { 44 | return Promise.resolve(); 45 | } 46 | 47 | return this.client.publish(userid.toString(), JSON.stringify({ 48 | type: type, 49 | object: object, 50 | actor: actor 51 | })); 52 | }; 53 | 54 | SYSEVENTS.prototype.listen = async function() { 55 | if (!config.redis) { 56 | return Promise.resolve({ 57 | close: function() {}, 58 | ondata: function(userid, cb) {} 59 | }); 60 | } 61 | 62 | let self = this; 63 | self.sub = self.client.duplicate(); 64 | return self.sub.connect().then(() => { 65 | return Promise.resolve({ 66 | close: function() { 67 | console.log('do close'); 68 | if (self.channel) { 69 | self.sub.unsubscribe(self.channel); 70 | } 71 | }, 72 | // only supports a single ondata channel, as store channel for close() 73 | ondata: function(userid, cb) { 74 | self.channel = userid.toString(); 75 | self.sub.subscribe(self.channel, (msg) => { 76 | cb(JSON.parse(msg)); 77 | }); 78 | } 79 | }); 80 | }); 81 | }; 82 | 83 | module.exports = SYSEVENTS; 84 | 85 | -------------------------------------------------------------------------------- /cubiti-server/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var safeb64 = require('url-safe-base64'); 4 | let Promise = require('bluebird'); 5 | let fs = require('fs'); 6 | let crypto = require('crypto'); 7 | const blurhashEncode = require("blurhash").encode; 8 | const imageDataGet = require('@andreekeberg/imagedata').get; 9 | var path = require('path'); 10 | var urlparser = require('url'); 11 | var fetch = require('node-fetch'); 12 | 13 | let util = module.exports; 14 | 15 | module.exports.imageFileToBlurhash = function(filename) { 16 | return new Promise((resolve, reject) => { 17 | imageDataGet(filename, (err, imageData) => { 18 | if (err) { 19 | reject(err); 20 | } else { 21 | resolve(blurhashEncode(imageData.data, imageData.width, imageData.height, 4, 4)); 22 | } 23 | }) 24 | }); 25 | }; 26 | 27 | module.exports.getMsgPrefix = function(app) { 28 | return `https://${app.config.server.domain}/m/`; 29 | } 30 | 31 | module.exports.decodeSafeB64 = function(data) { 32 | let buff = new Buffer(safeb64.decode(data.toString()), 'base64'); 33 | return buff.toString('ascii'); 34 | } 35 | 36 | module.exports.encodeSafeB64 = function(data) { 37 | let buff = Buffer.from(data).toString('base64'); 38 | return safeb64.encode(buff); 39 | } 40 | 41 | module.exports.userActor = function(app, user) { 42 | return `https://${app.config.server.domain}/u/${user.username}`; 43 | } 44 | 45 | module.exports.readFilePromise = function(filename) { 46 | return new Promise((resolve, reject) => { 47 | fs.readFile(filename, function(err, data) { 48 | if (err) { 49 | reject(err); 50 | } else { 51 | resolve(data); 52 | } 53 | }); 54 | }); 55 | } 56 | 57 | module.exports.unlinkFilePromise = function (filename) { 58 | return new Promise((resolve, reject) => { 59 | fs.unlink(filename, function(err) { 60 | if (err) { 61 | reject(err); 62 | } else { 63 | resolve(); 64 | } 65 | }); 66 | }); 67 | } 68 | 69 | module.exports.generateGuid = function generateGuid() { 70 | return new Promise((resolve, reject) => { 71 | crypto.randomBytes(16, (err, buf) => { 72 | if (err) { 73 | reject(err); 74 | } else { 75 | resolve(buf.toString('hex')); 76 | } 77 | }); 78 | }); 79 | } 80 | 81 | // An object has a url in object field, replace it with content 82 | module.exports.patchObject = function(app, body) { 83 | let url = body.object; 84 | 85 | // If the url is one of ours, get from db would end in /object 86 | if (body.object.startsWith(`${util.getMsgPrefix(app)}`)) { 87 | let [id, subtype] = body.object.replace(`${util.getMsgPrefix(app)}`, '').split('/'); 88 | return app.db.message_get(`${util.getMsgPrefix(app)}${id}`).then((messageData) => { 89 | body.object = messageData.message; 90 | return Promise.resolve(body); 91 | }); 92 | } else { 93 | return fetch(url, { 94 | method: 'GET', 95 | headers: { 96 | 'Accept': 'application/activity+json' 97 | } 98 | }).then((res) => { 99 | return res.json(); 100 | }).then((json) => { 101 | body.object = json; 102 | return Promise.resolve(body); 103 | }); 104 | } 105 | } 106 | 107 | // get status from db/http from given statusUrl 108 | // FIXME does no checking on permissions 109 | // FIXME does no caching 110 | module.exports.getObject = function(app, statusUrl) { 111 | let httpStatus; 112 | if (statusUrl.startsWith(`${util.getMsgPrefix(app)}`)) { 113 | // it's a message we have 114 | // Replace the prefix, leaving {guid} or {guid}/object 115 | let [id, type] = statusUrl.replace(`${util.getMsgPrefix(app)}`, '').split('/'); 116 | console.log("db message get ", `${util.getMsgPrefix(app)}${id}`); 117 | return app.db.message_get(`${util.getMsgPrefix(app)}${id}`).then((msgData) => { 118 | let msg = msgData.message; // The surrounding Create/Announce obj 119 | return Promise.resolve(msg.object); // return the Note 120 | }); 121 | } else { 122 | // a status from another server is being asked for, fetch it 123 | // FIXME check if we already have it in messages 124 | console.log("Fetching ", statusUrl); 125 | return fetch(statusUrl, { 126 | method: 'GET', 127 | headers: { 'Accept': 'application/activity+json' } 128 | }).then((res) => { 129 | httpStatus = res.status; 130 | if (res.status >= 200 && res.status < 300) { 131 | return res.text(); 132 | } else { 133 | return Promise.reject(`${statusUrl} http ${httpStatus}`); 134 | } 135 | }).then((text) => { 136 | return Promise.resolve(JSON.parse(text)); 137 | }); 138 | } 139 | } 140 | 141 | 142 | 143 | module.exports.msgToStatus = function(app, user, msg) { 144 | // console.log("******** OBJ", msg); 145 | // msg.type is Create || Announce 146 | let liked = false; 147 | let announced = false; 148 | let filterOut = false; 149 | 150 | return app.db.like_check(msg.object.id, util.userActor(app, user)).then((_liked) => { 151 | liked = _liked; 152 | return Promise.resolve(); 153 | }).then(() => { 154 | return app.db.announce_check(msg.object.id, util.userActor(app, user)).then((_announced) => { 155 | announced = _announced; 156 | return Promise.resolve(); 157 | }); 158 | }).then(() => { 159 | // don't want to see own reblogs 160 | if (msg.actor === util.userActor(app, user) && announced) { 161 | filterOut = true; 162 | return Promise.resolve(); 163 | } 164 | }).then(() => { 165 | return app.cmd.getActor(app, user, msg.object.attributedTo || msg.object.actor).then((actor) => { 166 | let attachments = []; 167 | if (msg.object.attachment) { 168 | msg.object.attachment.forEach((att) => { 169 | attachments.push({ 170 | id: att.url, 171 | type: att.mediaType.split('/')[0], // image/jpeg => image 172 | url: att.url, 173 | preview_url: att.url, 174 | remote_url: att.url, 175 | preview_remote_url: att.url, 176 | text_url: null, 177 | description: att.name, 178 | blurhash: att.blurhash 179 | }); 180 | }); 181 | } 182 | 183 | let st = { 184 | id: util.encodeSafeB64(msg.object.id), 185 | created_at: msg.object.published, 186 | in_reply_to_id: msg.object.inReplyTo ? util.encodeSafeB64(msg.object.inReplyTo) : null, 187 | in_reply_to_account_id: msg.object.attributedTo ? util.encodeSafeB64(msg.object.attributedTo) : null, 188 | sensitive: msg.object.sensitive || false, 189 | spoiler_text: msg.object.summary || '', 190 | visibility: 'public', 191 | language: null, 192 | uri: msg.object.id, 193 | url: msg.object.id, 194 | replies_count: 0, 195 | reblogs_count: 0, 196 | favourites_count: 0, 197 | edited_at: null, 198 | content: msg.object.content, 199 | reblog: null, 200 | account: util.actorToMastodonAccount(app, actor), 201 | media_attachments: attachments, 202 | mentions: [], 203 | tags: msg.object.tags || [], 204 | emojis: [], 205 | card: null, 206 | poll: null, 207 | favourited: liked, 208 | reblogged: announced, 209 | muted: false, 210 | bookmarked: false, 211 | filtered: [] 212 | }; 213 | 214 | if (msg.type === 'Create') { 215 | if (!filterOut) { 216 | return Promise.resolve(st); 217 | } else { 218 | return Promise.resolve(null); 219 | } 220 | } 221 | if (msg.type === 'Announce') { 222 | // get the Announcer's actor, status in reblog field 223 | if (msg.object.actor == util.userActor(app, user)) { 224 | return Promise.resolve(null); // don't show reblogs of own posts 225 | } 226 | return app.cmd.getActor(app, user, msg.actor).then((announcerActor) => { 227 | let wrapper = { 228 | id: encodeURI(msg.id), 229 | created_at: msg.published, 230 | in_reply_to_id: null, 231 | in_reply_to_account_id: null, 232 | sensitive: false, 233 | spoiler_text: '', 234 | visibility: 'public', 235 | language: null, 236 | uri: msg.id, 237 | url: msg.id, 238 | replies_count: 0, 239 | reblogs_count: 0, 240 | favourites_count: 0, 241 | edited_at: null, 242 | content: '', 243 | reblog: st, 244 | account: util.actorToMastodonAccount(app, announcerActor), 245 | media_attachments: [], 246 | mentions: [], 247 | tags: msg.object.tags || [], 248 | emojis: [], 249 | card: null, 250 | poll: null, 251 | favourited: false, 252 | reblogged: false, // FIXME, did we reblog this? 253 | muted: false, 254 | bookmarked: false, 255 | filtered: [] 256 | }; 257 | 258 | if (!filterOut) { 259 | return Promise.resolve(wrapper); 260 | } else { 261 | return Promise.resolve(null); 262 | } 263 | }); 264 | } 265 | }); 266 | }); 267 | } 268 | 269 | module.exports.actorToMastodonAccount = function(app, actor) { 270 | //console.log(actor); 271 | let actorUrl = urlparser.parse(actor.id); 272 | let actorUsername = path.basename(actorUrl.path); 273 | let actorHostname = actorUrl.host; 274 | let actorAccount = `${actorUsername}@${actorHostname}`; 275 | 276 | let avatar = 'https://' + app.config.server.domain + '/' + app.config.default_avatar; 277 | 278 | if (actor.icon && actor.icon.url) { 279 | avatar = actor.icon.url; 280 | } 281 | 282 | let safeActorId = util.encodeSafeB64(actor.id); 283 | 284 | return { 285 | id: safeActorId, 286 | username: actorUsername, 287 | acct: actorAccount, 288 | display_name: actor.preferredUsername, 289 | locked: false, 290 | bot: false, 291 | discoverable: false, 292 | group: false, 293 | created_at: actor.published || '2022-12-10T00:00:00.000Z', 294 | note: actor.summary || '', 295 | url: actor.id, 296 | avatar: avatar, 297 | avatar_static: avatar, 298 | header: 'https://' + app.config.server.domain + '/' + app.config.default_header, 299 | header_static: 'https://' + app.config.server.domain + '/' + app.config.default_header, 300 | followers_count: 0, 301 | following_count: 0, 302 | statuses_count: 1, 303 | last_status_at: '2022-12-12', 304 | emojis: [], 305 | fields: [] 306 | }; 307 | } 308 | 309 | module.exports.createActor = function(username, domain, pubkey) { 310 | return { 311 | '@context': [ 312 | 'https://www.w3.org/ns/activitystreams', 313 | 'https://w3id.org/security/v1' 314 | ], 315 | 'id': `https://${domain}/u/${username}`, 316 | 'type': 'Person', 317 | 'preferredUsername': `${username}`, 318 | 'inbox': `https://${domain}/u/${username}/inbox`, 319 | 'followers': `https://${domain}/u/${username}/followers`, 320 | 'publicKey': { 321 | 'id': `https://${domain}/u/${username}#main-key`, 322 | 'owner': `https://${domain}/u/${username}`, 323 | 'publicKeyPem': pubkey 324 | } 325 | }; 326 | } 327 | 328 | 329 | -------------------------------------------------------------------------------- /cubiti-server/views/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /cubiti-server/websock.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let createServer = require('http').createServer; 4 | let WebSocketServer = require('ws').WebSocketServer; 5 | var urlparser = require('url'); 6 | const querystring = require("querystring"); 7 | let util = require('./util.js'); 8 | let Promise = require('bluebird'); 9 | let SYSEVENTS = require('./sysevents.js'); 10 | 11 | 12 | function getStatuses(app, user) { 13 | return app.db.timeline(user.userid, util.userActor(app, user)).then((messageDatas) => { 14 | let statuses = []; 15 | console.log("timeline len =", messageDatas.length); 16 | 17 | return Promise.each(messageDatas, function(messageData, index, arrayLength) { 18 | return util.msgToStatus(app, user, messageData.message).then((st) => { 19 | if (st) { // msgToStatus may return null to filter message out 20 | statuses.push(st); 21 | } 22 | return Promise.resolve(); 23 | }); 24 | }).then(() => { 25 | return Promise.resolve(statuses); 26 | }); 27 | }); 28 | } 29 | 30 | // convert sysevent to Mastodon websocket message 31 | function evtToWsMsg(app, user, evMsg) { 32 | let rsp = null; 33 | //console.log("evtToWsMsg", evMsg); 34 | if (evMsg.type === 'update' || evMsg.type === 'updateannounce') { // object is a message, convert to Status 35 | if (!evMsg.object) { 36 | return Promise.reject('no object'); 37 | } 38 | return util.getObject(app, evMsg.object).then((msg) => { 39 | // fetched a Note, but util.msgToStatus is expecting a Create with embedded Note 40 | return util.msgToStatus(app, user, { 41 | id: evMsg.object, 42 | actor: evMsg.type === 'update' ? msg.attributedTo : evMsg.actor, 43 | object: msg, 44 | type: evMsg.type === 'update' ? 'Create' : 'Announce', 45 | published: msg.published 46 | }).then((st) => { 47 | if (st) { // msgToStatus may return null to filter message out 48 | rsp = { 49 | "stream": ['user'], 50 | "event": 'update', 51 | "payload": JSON.stringify(st) 52 | }; 53 | } else { 54 | console.log("FILTEREDOUT"); 55 | } 56 | }); 57 | }).then(() => { 58 | return Promise.resolve(rsp); 59 | }); 60 | } 61 | if (evMsg.type === 'delete') { // actor id only 62 | if (!evMsg.actor) { 63 | return Promise.reject('no actor'); 64 | } 65 | return Promise.resolve({ 66 | "stream": ['user'], 67 | "event": 'delete', 68 | "payload": util.encodeSafeB64(evMsg.actor) 69 | }); 70 | } 71 | 72 | if (evMsg.type === 'announce' || evMsg.type === 'like') { // object and actor 73 | let actorData; 74 | if (!evMsg.object || !evMsg.actor) { 75 | return Promise.reject('no actor or object'); 76 | } 77 | // turn actor url into actor object 78 | return app.cmd.getActor(app, user, evMsg.actor).then((_actorData) => { 79 | actorData = _actorData; 80 | }).then(() => { 81 | return util.getObject(app, evMsg.object).then((msg) => { 82 | // fetched a Note, but util.msgToStatus is expecting a Create with embedded Note 83 | return util.msgToStatus(app, user, { 84 | id: evMsg.object, 85 | actor: msg.attributedTo, 86 | object: msg, 87 | type: "Create", 88 | published: msg.published 89 | }).then((st) => { 90 | //return util.msgToStatus(app, user, message).then((st) => { 91 | if (st) { // msgToStatus may return null to filter message out 92 | rsp = { 93 | "stream": ['user'], 94 | "event": 'notification', 95 | "payload": JSON.stringify({ 96 | "status": st, 97 | "id": util.encodeSafeB64(evMsg.object), 98 | "type": evMsg.type == 'announce' ? 'reblog' : 'favourite', 99 | "created_at": new Date(msg.published).toISOString(), 100 | "account": util.actorToMastodonAccount(app, actorData) 101 | }) 102 | }; 103 | } 104 | }); 105 | }).then(() => { 106 | return Promise.resolve(rsp); 107 | }); 108 | }); 109 | } 110 | 111 | return Promise.reject('unknown/failed evt type ' + evMsg.type); 112 | } 113 | 114 | function handle_stream(app, ws, req, user) { 115 | var ev = new SYSEVENTS(); 116 | return ev.open().then(() => { 117 | return ev.listen().then((listener) => { 118 | listener.ondata(user.userid, (evMsg) => { 119 | console.log("WS sysevent received", evMsg); 120 | evtToWsMsg(app, user, evMsg).then((wsMsg) => { 121 | //console.log("WSSEND", wsMsg); 122 | ws.send(JSON.stringify(wsMsg)); 123 | }).catch((err) => { 124 | console.log("Unable to process evt", err); 125 | }); 126 | }); 127 | }).then(() => { 128 | return ev; 129 | }); 130 | }); 131 | } 132 | 133 | 134 | module.exports.start = function(app) { 135 | const server = createServer(); 136 | const wss = new WebSocketServer({ noServer: true }); 137 | 138 | function authenticate(req, cb) { 139 | const url = urlparser.parse(req.url); 140 | let qs = querystring.parse(url.query); 141 | 142 | if (qs.access_token === undefined) { 143 | console.log("/api/v1/streaming no token"); 144 | cb('no token', null); 145 | } else { 146 | app.db.user_validateToken(qs.access_token).then((user) => { 147 | if (user === null) { 148 | console.log("/api/v1/streaming Unknown token " + qs.access_token); 149 | cb('bad token', null); 150 | } else { 151 | console.log("WS auth ok"); 152 | cb(null, user); 153 | } 154 | }); 155 | } 156 | } 157 | 158 | server.on('upgrade', function upgrade(request, socket, head) { 159 | authenticate(request, function next(err, user) { 160 | if (err || !user) { 161 | socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); 162 | socket.destroy(); 163 | return; 164 | } 165 | 166 | wss.handleUpgrade(request, socket, head, function done(ws) { 167 | wss.emit('connection', ws, request, user); 168 | }); 169 | }); 170 | }); 171 | 172 | wss.on('connection', function connection(ws, req, user) { 173 | console.log("USER", user.userid, user.username); 174 | handle_stream(app, ws, req, user).then((evconn) => { 175 | ws.on('message', function message(data) { 176 | console.log(`Received message ${data} from user ${client}`); 177 | }); 178 | ws.on('close', function close() { 179 | console.log('ws disconnected'); 180 | evconn.close(); 181 | }); 182 | }).catch((err) => { 183 | console.log("Failed to stream", err); 184 | ws.close(); 185 | }); 186 | }); 187 | 188 | console.log("WS 8001"); 189 | server.listen(8001); 190 | 191 | } 192 | 193 | -------------------------------------------------------------------------------- /data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "domain": "localhost", 4 | "title": "cubiti", 5 | "short_description": "cubiti, a toy fediverse server", 6 | "description": "Mastodon in the front, ActivityPub in the back!", 7 | "email": "admin@localhost", 8 | "adminuser": "cubiti", 9 | "thumbnail": "images/cubiti-logo-icon.png" 10 | }, 11 | "redis": "redis://redis:6379", 12 | "default_avatar": "images/cubiti-logo-icon.png", 13 | "default_header": "images/cubiti-logo.png", 14 | "supported_mime_types": [ 15 | "image/jpeg", 16 | "image/png", 17 | "image/gif" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | 4 | services: 5 | cubiti-server: 6 | build: cubiti-server 7 | image: cubiti-server 8 | stdin_open: true 9 | tty: true 10 | container_name: cubiti 11 | restart: unless-stopped 12 | security_opt: 13 | - no-new-privileges:true 14 | volumes: 15 | - ./data:/data 16 | 17 | redis: 18 | image: redis 19 | container_name: redis 20 | restart: unless-stopped 21 | security_opt: 22 | - no-new-privileges:true 23 | 24 | nginx-alpine-ssl: 25 | build: nginx-alpine-ssl 26 | image: nginx-alpine-ssl 27 | container_name: nginx-alpine-ssl 28 | depends_on: 29 | - cubiti-server 30 | restart: always 31 | security_opt: 32 | - no-new-privileges:true 33 | ports: 34 | - "443:443" 35 | 36 | -------------------------------------------------------------------------------- /nginx-alpine-ssl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk add nginx 3 | RUN mkdir -p /run/nginx 4 | ADD default.conf /etc/nginx/http.d/default.conf 5 | ADD *.key /etc/ssl/private/ 6 | ADD *.crt /etc/ssl/certs/ 7 | WORKDIR /var/www/localhost/htdocs 8 | COPY entrypoint.sh /usr/local/bin 9 | RUN chmod +x /usr/local/bin/entrypoint.sh 10 | ENTRYPOINT ["/bin/sh", "/usr/local/bin/entrypoint.sh"] 11 | #EXPOSE 80 12 | EXPOSE 443 13 | CMD ["/bin/sh", "-c", "nginx -g 'daemon off;'; nginx -s reload;"] 14 | -------------------------------------------------------------------------------- /nginx-alpine-ssl/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server; 4 | listen 443 ssl http2 default_server; 5 | listen [::]:443 ssl http2 default_server; 6 | ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; 7 | ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; 8 | 9 | location /api/v1/streaming { 10 | proxy_http_version 1.1; 11 | proxy_set_header Upgrade $http_upgrade; 12 | proxy_set_header Connection "Upgrade"; 13 | proxy_redirect off; 14 | proxy_connect_timeout 600; 15 | proxy_send_timeout 600; 16 | proxy_read_timeout 600; 17 | proxy_set_header Host $host; 18 | proxy_set_header X-Real-IP $remote_addr; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | proxy_set_header X-Forwarded-Proto $scheme; 21 | proxy_set_header Proxy ""; 22 | proxy_pass_header Server; 23 | proxy_buffering on; 24 | tcp_nodelay on; 25 | proxy_pass http://cubiti:8001; 26 | proxy_set_header Host $http_host; 27 | } 28 | 29 | location / { 30 | proxy_http_version 1.1; 31 | proxy_set_header Upgrade $http_upgrade; 32 | proxy_set_header Connection "Upgrade"; 33 | proxy_redirect off; 34 | proxy_connect_timeout 90; 35 | proxy_send_timeout 90; 36 | proxy_read_timeout 90; 37 | proxy_set_header Host $host; 38 | proxy_set_header X-Real-IP $remote_addr; 39 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 40 | proxy_set_header X-Forwarded-Proto $scheme; 41 | proxy_set_header Proxy ""; 42 | proxy_pass_header Server; 43 | proxy_buffering on; 44 | tcp_nodelay on; 45 | proxy_pass http://cubiti:8002; 46 | proxy_set_header Host $http_host; 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /nginx-alpine-ssl/entrypoint.sh: -------------------------------------------------------------------------------- 1 | cd /etc/nginx/http.d; 2 | export CRT="${CRT:=nginx-selfsigned.crt}"; 3 | if [ -f "/etc/ssl/certs/$CRT" ] 4 | then 5 | # set crt file in the default.conf file 6 | sed -i "/ssl_certificate \//c\\\tssl_certificate \/etc\/ssl\/certs\/$CRT;" default.conf; 7 | fi 8 | export KEY="${KEY:=nginx-selfsigned.key}"; 9 | if [ -f "/etc/ssl/private/$KEY" ] 10 | then 11 | # set key file in the default.conf file 12 | sed -i "/ssl_certificate_key \//c\\\tssl_certificate_key \/etc\/ssl\/private\/$KEY;" default.conf; 13 | fi 14 | nginx -g 'daemon off;'; nginx -s reload; 15 | 16 | -------------------------------------------------------------------------------- /nginx-alpine-ssl/nginx-selfsigned.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDjDCCAnSgAwIBAgIUCCSqvSfnCK67C4JNfoiXUXyTIK4wDQYJKoZIhvcNAQEL 3 | BQAwSTELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAlFDMRYwFAYDVQQKDA1Db21wYW55 4 | LCBJbmMuMRUwEwYDVQQDDAxteWRvbWFpbi5jb20wHhcNMjIxMjEyMTAwNzU1WhcN 5 | MjMxMjEyMTAwNzU1WjBJMQswCQYDVQQGEwJDQTELMAkGA1UECAwCUUMxFjAUBgNV 6 | BAoMDUNvbXBhbnksIEluYy4xFTATBgNVBAMMDG15ZG9tYWluLmNvbTCCASIwDQYJ 7 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBANss2w/GUwKcoUxHsWDfnldEuJzwx3Jr 8 | oRvTZY7ZcEM8vsVW8Xi61jpo2H/Uqv+3jl6+R6UFL1IKQUY0jn9KatYkfrHdHcYx 9 | RwH8yLKWfCY9/qrPE8NzYQMkeNUqu5oGWDMFoCcGAuHOzB+v6JR2/0zaEavi96dZ 10 | ZwjijdZtZAB9BuqD5R5dmVBV1fYSWM/X0/KN2RPpoBRak+HmpoZfimut9rMAPjay 11 | WjVxQCR/kCL6OlfLL5CFp6e6u9pczRNTLr0QODmyQGIBd4Rjh1JQD2K1c1QN4ztw 12 | ExGW+gqe7CGuwVfPSjlUsE1kiC11KreAWadLiovOp4Th6lygeaYg4R8CAwEAAaNs 13 | MGowHQYDVR0OBBYEFE/ykxo/J5z2IT9Zuk3uwk+NAb4eMB8GA1UdIwQYMBaAFE/y 14 | kxo/J5z2IT9Zuk3uwk+NAb4eMA8GA1UdEwEB/wQFMAMBAf8wFwYDVR0RBBAwDoIM 15 | bXlkb21haW4uY29tMA0GCSqGSIb3DQEBCwUAA4IBAQA/8ptI9ncISkYBDz8hUmWE 16 | WkBsSFs2BTBvUQ4bsdXmV3AkC1BFw0meW3kNL/4ptkSsOvVj4imBjG906UfyXw5l 17 | TyegRn2pA13IqBgl0Fs0+qlg5a/a+UgMZHmJeCsOZ3gJCG/mqJ0MyE8vUCUcD1oZ 18 | XGsUgOUkiK/eMN6r4kW6SsBs7iapDpascvmGz4VuzYpBy+qOGayfCOt4h/hS9VEC 19 | ErZo1L6jJFBApM1Jxmd7yYWJeQAkN1/LjdYJltSZ4dNlw6ewzK/Px0hGeEzr60M7 20 | +JgGuAuxIdp3pTYZwB5TqZ5v/bvapzPgK4A7COBOj1N3uqJiTmYErKwx201AP3BQ 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /nginx-alpine-ssl/nginx-selfsigned.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDbLNsPxlMCnKFM 3 | R7Fg355XRLic8Mdya6Eb02WO2XBDPL7FVvF4utY6aNh/1Kr/t45evkelBS9SCkFG 4 | NI5/SmrWJH6x3R3GMUcB/MiylnwmPf6qzxPDc2EDJHjVKruaBlgzBaAnBgLhzswf 5 | r+iUdv9M2hGr4venWWcI4o3WbWQAfQbqg+UeXZlQVdX2EljP19PyjdkT6aAUWpPh 6 | 5qaGX4prrfazAD42slo1cUAkf5Ai+jpXyy+QhaenurvaXM0TUy69EDg5skBiAXeE 7 | Y4dSUA9itXNUDeM7cBMRlvoKnuwhrsFXz0o5VLBNZIgtdSq3gFmnS4qLzqeE4epc 8 | oHmmIOEfAgMBAAECggEBAJDckN1YQ71SMPnt2LsikdE0RqDUM77YjF+L1XAZHy4R 9 | lDVyRZ96PeXVLmMu+OaTN7I/KbNUPfaHeKUiT5yqXvqynFqKvwcjwr75iN0gwWW1 10 | TAExZOql89TT4lliKSSgVONEMJoaSwVcXWYEKkEWdZ8h8tQc63rciFFDDGRRYOtA 11 | fmMb3tOmnJqGu4PDq4vnVv7YiCXvNZiVOz99AsW0Y1ptSMyQrxyLjdr+wxClh0UV 12 | uGFcFIJJwsvBGDNb6G3Wy3vJHkkqMEhPwfP/AkHZMdQKdZ15V/WAOP8xKXW205jY 13 | Lu0mCbv2Udaait+fjZhM/JoemPLApwLNVRpwV5QfGwECgYEA9X/fjVPhJZ42LrP0 14 | Z4j2tj47DLtHLktrd84OA4BV4I+JjTvddJfXCtEk1m59vpzutJEYpy/bII84JWuE 15 | H1cMv8epS4Yfi/2RoB8ADO7E0L/BPAND7zjCHIqryiZY7ubp/71/jaOF0ZCugqbi 16 | YK7sl9H7qj1u+cC4+pab9ue/IyECgYEA5Iy90M7f7bI+6tS2/k4eroLxGWAJqRSj 17 | D2DjYTd/gPgm8jCDhnmbicquP2YBTIIdaNiREh19pvQs/JRo+tbsGKgSQbjLdM8Z 18 | 8WzmhrNJH/fF/Vmi8DYSg4VScZgyjJX5T1FsRup8r53hxVpyRtTEJLOzSfJDEE1L 19 | eb09EeHrvD8CgYAOKdt25uD1b6RGm4E9O+yn5P05JdDcfeNsXQn3776EnyNbb5m+ 20 | MUhpylkqueMtTRaEel6Gvr8QqNKfbg2IVVhZ9CXzQoCtbeqp5z/0fw4B0R5P3Qxd 21 | T9P7G5D/r6iv18imRYOHY2jEB2naBdDHrS/fLnEriDHP3OuPIYNMAmDHoQKBgQCQ 22 | Py/yIQ9+Axjot7aDTKTaubQXsuCGAYtkwl7gVdm4eWaDRxFMB2aekfhl9ShutFSB 23 | fuYYy9opTEU0aSrU3l8GtNVI+6wVnjyefoAElhVaAtTIMRHAkDAhKD0/irKkvmcq 24 | o5Y2L/rgEEKVf59Oiyz8iRpoWmnvWQmA3Wo05iUVmwKBgHTh1q1PTUzgvL0uNNZ3 25 | Kttp/U81I0C0TEyLFt/WfAD6ZrsG3GMq5IqN2CkOvPSDCrdxAxiDuxK7l3/gWU6s 26 | 9EtoG2gZb5SyU6hZ0isuokaeAyuueDEco38AFXSvmt/jxvdzilYW/n5+HNoV2XL1 27 | CBv1Y6Ouy7rA3Q9C7WPb43m4 28 | -----END PRIVATE KEY----- 29 | --------------------------------------------------------------------------------