Skip to content
Permalink
Browse files

Create the server, web, and test files.

Create the backend server files, the frontend files, and tests for the
backend. Include `package.json` and `.babelrc` configuration files. Also
include an ESLint configuration file.

Use Express.js for frontend hosting, and use Mocha.js for testing.
Firebase is used as the database for the service.Use Handlebars for
templating. The server also exposes documentation files and a public
API.
  • Loading branch information
skunkmb committed Jan 6, 2019
1 parent 9c41ed9 commit 82be5c30e7142a47337278d11ddc1883b7b7b03b
Showing with 11,062 additions and 0 deletions.
  1. +14 −0 .babelrc
  2. +7,389 −0 package-lock.json
  3. +40 −0 package.json
  4. +30 −0 server/.eslintrc
  5. +12 −0 server/config/firebaseServiceKey.json
  6. +45 −0 server/config/settings.json
  7. +24 −0 server/database.js
  8. +227 −0 server/link-statistics.js
  9. +36 −0 server/long-links.js
  10. +214 −0 server/main.js
  11. +256 −0 server/short-links.js
  12. +143 −0 server/templates.js
  13. +187 −0 server/total-statistics.js
  14. +73 −0 server/type-definitions.js
  15. +115 −0 server/util.js
  16. +121 −0 tests/link-statistics.test.js
  17. +53 −0 tests/long-links.test.js
  18. +230 −0 tests/short-links.test.js
  19. +92 −0 tests/test-util.js
  20. +68 −0 tests/total-statistics.test.js
  21. +51 −0 tests/util.test.js
  22. +428 −0 web/static/css/index.css
  23. +4 −0 web/static/css/mobile.css
  24. +38 −0 web/static/css/statistic.css
  25. +3 −0 web/static/js/src/config.js
  26. +110 −0 web/static/js/src/index.js
  27. +83 −0 web/static/js/src/statistic.js
  28. BIN web/static/res/drawings/android-drawing.png
  29. BIN web/static/res/drawings/discord-drawing.png
  30. BIN web/static/res/drawings/github-drawing.png
  31. BIN web/static/res/drawings/ios-drawing.png
  32. BIN web/static/res/drawings/js-library-drawing.png
  33. BIN web/static/res/drawings/telegram-drawing.png
  34. BIN web/static/res/drawings/web-api-drawing.png
  35. BIN web/static/res/hero-photo.jpeg
  36. BIN web/static/res/koala-favicon.ico
  37. BIN web/static/res/koala-logo-white.png
  38. BIN web/static/res/koala-logo.png
  39. BIN web/static/res/mont.otf
  40. BIN web/static/res/readme-header.png
  41. BIN web/static/res/spacemono.ttf
  42. BIN web/static/res/young.ttf
  43. +33 −0 web/views/404.hbs
  44. +60 −0 web/views/discord-bot.hbs
  45. +231 −0 web/views/index.hbs
  46. +182 −0 web/views/js-library.hbs
  47. +126 −0 web/views/mobile.hbs
  48. +91 −0 web/views/statistic.hbs
  49. +64 −0 web/views/telegram-bot.hbs
  50. +189 −0 web/views/web-api.hbs
@@ -0,0 +1,14 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": "cover 95%, last 2 versions"
}
],
[
"minify"
]
],
"comments": false
}

Large diffs are not rendered by default.

@@ -0,0 +1,40 @@
{
"name": "qwala-server",
"version": "1.0.0",
"description": "an open-source link shortener",
"main": "app.js",
"scripts": {
"start": "npm run build && node ./server/main.js",
"build": "babel ./web/static/js/src/ -d ./web/static/js/dist/",
"test": "mocha tests/"
},
"author": "Marco Burstein <marco@marco.how>",
"license": "GPL-3.0-only",
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.4",
"firebase-admin": "^6.4.0",
"handlebars": "^4.0.12",
"iplocation": "^6.0.4",
"lodash": "^4.17.11",
"request-ip": "^2.1.3",
"serve-favicon": "^2.5.0",
"url-regex": "^4.1.1"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.3",
"babel-preset-minify": "^0.5.0",
"eslint": "^5.11.1",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.2",
"eslint-plugin-react": "^7.12.2",
"mocha": "^5.2.0",
"mock-require": "^3.0.2"
},
"engines": {
"node": "10"
}
}
@@ -0,0 +1,30 @@
{
"extends": "airbnb",
"rules": {
"indent": ["error", 4],
"prefer-const": "off",
"max-len": ["error", {
"code": 79,
"ignoreUrls": true,
"ignoreComments": false,
"ignoreTrailingComments": false,
"ignoreStrings": false,
"ignoreRegExpLiterals": true
}],
"semi": ["error", "always"],
"arrow-body-style": ["error", "always"],
"yoda": ["error", "never"],
"function-paren-newline": "off",
"newline-per-chained-call": "off",
"prefer-template": "off",
"no-plusplus": "off",
"no-throw-literal": "off",
"prefer-destructuring": "off",
"no-use-before-define": "off"
},
"env": {
"browser": true,
"node": true,
"jquery": true
}
}
@@ -0,0 +1,12 @@
{
"type": "service_account",
"project_id": "<put your own Project ID here>",
"private_key_id": "<put your own Private Key ID here>",
"private_key": "<put your own Private Key here>",
"client_email": "<put your own Client Email Address here>",
"client_id": "<put your own Client ID here>",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "<put your own Certificate URL here>"
}
@@ -0,0 +1,45 @@
{
"shortLinkAdjectives": [
"red", "orange", "yellow", "green", "blue", "purple",
"friendly", "smart", "nice", "old", "young", "strong", "big", "small",
"tall", "short", "thin", "kind"
],
"shortLinkNouns": [
"dog", "cat", "fish", "bison", "dolphin", "eagle", "pony", "ape",
"monkey", "cow", "duck", "rabbit", "spider", "wolf", "turkey",
"lion", "pig", "snake", "shark", "bird", "bear", "fish", "chicken",
"horse", "cat", "dog", "koala"
],
"shortLinkVerbs": [
"walking", "running", "having", "doing", "saying", "going", "getting",
"making", "knowing", "thinking", "taking", "seeing", "wanting",
"looking", "using", "finding", "giving", "telling", "working",
"calling", "trying", "asking", "needing", "feeling", "keeping",
"beginning", "talking", "helping", "turning"
],
"shortLinkUnambiguousDigits": [
"2", "3", "4", "5", "6", "7", "8", "9"
],
"shortLinkUnambiguousLetters": [
"a", "b", "c", "d", "e", "f", "g", "h", "j", "k", "m", "n", "p", "q",
"r", "t", "w", "x", "y", "z"
],
"reservedCustomShortLinkIDRegExps": [
"^pages$", "^info$", "^information$", "^terms$", "^about$", "^settings$",
"^qwala$", "^qwa.la$", "^faq$", "^report$", "^contact$", "^news$", "^static$",
"^mobile$", "^ios$", "^android$", "^api$", "^telegram-bot$",
"^discord-bot$", "^telegram$", "^discord$", "^repository$", "^github$",
"^web-api$", "^js-library$", "^js$", "^javascript$", "^contact$",
"^settings$", "^developer$", "^developers$"
],
"reservedLongLinkRegExps": [
"^http://qwa.la($|/)", "^https://qwa.la($|/)"
],
"customShortLinkIDRegExp": "^[a-zA-Z\\-_0-9]+$",
"shortLinkCharactersLength": 6,
"protocolRegExp": "^(https://|http://)",
"defaultTotalViewCount": 500,
"defaultTotalLinksCount": 200,
"defaultTotalCustomLinksCount": 100,
"analyticsKey": "<put your own Google Analytics Key here>"
}
@@ -0,0 +1,24 @@
/**
* A module that contains database-related functions.
*
* @module database
*/

let firebase = require('firebase-admin');
let serviceKey = require('./config/firebaseServiceKey.json');

firebase.initializeApp({
credential: firebase.credential.cert(serviceKey),
databaseURL: `https://${serviceKey.project_id}.firebaseio.com`,
});

firebase.firestore().settings({ timestampsInSnapshots: true });

/**
* Returns a configured Firebase Firestore reference.
*
* @return {Object} The Firestore.
*/
module.exports.get = function getDatabase() {
return firebase.firestore();
};
@@ -0,0 +1,227 @@
/**
* A module that contains functions for managing link statistics.
*
* @module link-statistics
*/

let ipLocation = require('iplocation').default;
let shortLinks = require('./short-links.js');
let util = require('./util.js');

/**
* Returns statistics for this week (the last 7 days) for a short link ID.
*
* @param {string} shortLinkID The short link ID to find statistics for.
* @return {LabelDataArrayPair} The statistics for weekly views.
*/
let getWeekly = async function getWeeklyStatistics(shortLinkID) {
let weekAgo = new Date();
weekAgo.setHours(0, 0, 0, 0);
weekAgo.setDate(weekAgo.getDate() - 6);

let views = await shortLinks.getViews(shortLinkID);
let weeklyViews = views.filter((view) => {
return weekAgo <= view.viewed;
});

let labels = getWeekLabels(weekAgo);
let data = getDataArray(weeklyViews, weekAgo);

return { labels, data };
};

/**
* Returns statistics for this month (the last 31 days) for a short link ID.
*
* @param {string} shortLinkID The short link ID to find statistics for.
* @return {LabelDataArrayPair} The statistics for monthly views.
*/
let getMonthly = async function getMonthlyStatistics(shortLinkID) {
let monthAgo = new Date();
monthAgo.setHours(0, 0, 0, 0);
monthAgo.setDate(monthAgo.getDate() - 30);

let views = await shortLinks.getViews(shortLinkID);
let monthlyViews = views.filter((view) => {
return monthAgo <= view.viewed;
});

let labels = getLabelsSinceDate(monthAgo);
let data = getDataArray(monthlyViews, monthAgo);

return { labels, data };
};

/**
* Returns statistics for all time for a short link ID.
*
* @param {string} shortLinkID The short link ID to find statistics for.
* @return {LabelDataArrayPair} The statistics for all time.
*/
let getAll = async function getAllStatistics(shortLinkID) {
let views = await shortLinks.getViews(shortLinkID);
let creationDate = await shortLinks.getCreationDate(shortLinkID);

let labels = getLabelsSinceDate(creationDate);
let data = getDataArray(views, creationDate);

return { labels, data };
};

/**
* Returns a map of views for all time for a short link ID.
*
* @param {string} shortLinkID The short link ID to find the map for.
* @return {Array<MapDataPoint>} An array of map points to use for the map.
*/
let getMap = async function getAllStatistics(shortLinkID) {
let views = await shortLinks.getViews(shortLinkID);
return getMapArray(views);
};

/**
* Returns an array containing values for the functions `getWeekly`,
* `getMonthly`, `getAll`, and `getMap`.
*
* @param {string} shortLinkID The short link ID to find the statistics and map
* for.
* @return {LinkStatisticsSummary} The array containing the values.
*/
module.exports.getEach = async function getEachStatisticArray(shortLinkID) {
return Promise.all([
getWeekly(shortLinkID),
getMonthly(shortLinkID),
getAll(shortLinkID),
getMap(shortLinkID),
]);
};

/**
* Returns a list of labels for weekly statistics.
*
* @param {Date} startDate The date that the week starts on.
* @return {Array<string>} An array of strings representing labels for each day
* of the week.
*/
let getWeekLabels = function getWeekLabelsForIndex(startDate) {
let days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
let startSlice = days.slice(startDate.getDay());
let endSlice = days.slice(0, startDate.getDay());
return startSlice.concat(endSlice);
};

/**
* Returns a list of labels for the days since a specified day.
*
* @param {Date} startDate The date to use as the starting label.
* @return {Array<string>} An array of labels for each day until today, where
* index 0 corresponds with `startDate`.
*/
let getLabelsSinceDate = function getDateLabelsSinceDate(startDate) {
let currentDates = [];
let todayDate = new Date();
todayDate.setHours(0, 0, 0, 0);

for (let i = 0; true; i++) {
let currentDate = new Date(startDate.getTime());
currentDate.setDate(currentDate.getDate() + i);
currentDate.setHours(0, 0, 0, 0);

currentDates.push(getDateString(currentDate));

if (todayDate <= currentDate) {
return currentDates;
}
}
};

/**
* Returns a map of views for all time for an array of views.
*
* @param {Array<View>} views The views to use to construct the map.
* @return {Array<MapDataPoint>} An array of map points to use for the map.
*/
let getMapArray = async function getLatitudeLongitudeArray(views) {
let points = await Promise.all(
views.map(async (view) => {
// `iplocation` seems to have a bug where return data is empty, but
// this can be solved by using only one of its default providers.
// See https://git.io/fh3hm.
let ipProviders = [
'https://ipinfo.io/*',
];

let lookup = await new Promise((resolve) => {
ipLocation(view.ipAddress, ipProviders, (error, response) => {
if (error) {
resolve({});
return;
}

resolve(response);
});
});

if (
!lookup
|| lookup.latitude === null
|| lookup.longitude === null
) {
return null;
}

return {
latitude: lookup.latitude,
longitude: lookup.longitude,
title: getDateString(view.viewed),
};
}),
);

return points.filter((point) => {
return point !== null;
});
};

/**
* Returns a data array for an array of views given a start date.
*
* @param {Array<View>} views An array of views for a link.
* @param {Date} startDate The date to start with in the data array. All views
* before this date are ignored.
* @return {Array<number>} An array containing numbers representing the number
* of views for each day, where index 0 corresponds
* with `startDate`.
*/
let getDataArray = function getDataArrayForViews(views, startDate) {
// `dataLength` is the number of data points (i.e. days) to be shown.
// E.g., for a month this would be 31, and for a week this would be 7.
let dataLength = util.getDateDifference(startDate, new Date()) + 1;
let currentData = new Array(dataLength).fill(0);

for (let i = 0; i < views.length; i++) {
// The data index is the location in the data array, which is
// essentially the difference between the day index for this one view
// and the starting day.
let dataDayIndex = util.getDateDifference(startDate, views[i].viewed);

currentData[dataDayIndex]++;
}

return currentData;
};

/**
* Returns a string representation of a date, in the form "<month>_<date>", for
* a `LabelDataArrayPair`.
*
* @param {Date} date The date to return a string for.
* @return {string} The string representation.
*/
let getDateString = function getDateAndMonthString(date) {
let months = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
'Oct', 'Nov', 'Dec',
];
return months[date.getMonth()] + '_' + date.getDate();
};

0 comments on commit 82be5c3

Please sign in to comment.
You can’t perform that action at this time.