├── public
├── images
│ ├── corn.jpg
│ ├── dirt.jpg
│ ├── beans.jpg
│ ├── beets.jpg
│ ├── onions.jpg
│ ├── wheat.jpg
│ ├── potatoes.jpg
│ ├── pumpkins.jpg
│ ├── tomatoes.jpg
│ ├── bellpeppers.jpg
│ ├── chilipeppers.jpg
│ ├── strawberries.jpg
│ └── coin.svg
└── stylesheets
│ └── style.css
├── notifications
├── welcome.utml
├── rotten.utml
├── dehydration.utml
├── emptyplot.utml
├── ripecrop.utml
├── reallyneedswater.utml
├── needswater.utml
└── overripe.utml
├── views
├── plotpage.utml
├── plant.utml
├── about.utml
├── buy-plot.utml
├── water.utml
├── croppage.utml
├── tearup.utml
├── harvest.utml
├── login.utml
├── plot.utml
├── index.utml
├── nav-anonymous.utml
├── croptype.utml
├── nav-loggedin.utml
├── farmer.utml
├── crop.utml
└── layout.utml
├── .gitignore
├── package.json
├── models
├── openfarmgame.js
├── requesttoken.js
├── plot.js
├── crop.js
├── croptype.js
├── host.js
└── farmer.js
├── README.md
├── data
└── croptypes.json
├── lib
├── notifier.js
└── updater.js
├── Apache-2.0
├── app.js
└── routes
└── index.js
/public/images/corn.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/corn.jpg
--------------------------------------------------------------------------------
/public/images/dirt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/dirt.jpg
--------------------------------------------------------------------------------
/public/images/beans.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/beans.jpg
--------------------------------------------------------------------------------
/public/images/beets.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/beets.jpg
--------------------------------------------------------------------------------
/public/images/onions.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/onions.jpg
--------------------------------------------------------------------------------
/public/images/wheat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/wheat.jpg
--------------------------------------------------------------------------------
/public/images/potatoes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/potatoes.jpg
--------------------------------------------------------------------------------
/public/images/pumpkins.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/pumpkins.jpg
--------------------------------------------------------------------------------
/public/images/tomatoes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/tomatoes.jpg
--------------------------------------------------------------------------------
/public/images/bellpeppers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/bellpeppers.jpg
--------------------------------------------------------------------------------
/public/images/chilipeppers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/chilipeppers.jpg
--------------------------------------------------------------------------------
/public/images/strawberries.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e14n/openfarmgame/HEAD/public/images/strawberries.jpg
--------------------------------------------------------------------------------
/notifications/welcome.utml:
--------------------------------------------------------------------------------
1 |
2 | Welcome to <%- game.displayName %> !
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 50px;
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4 | }
5 |
6 | a {
7 | color: #00B7FF;
8 | }
--------------------------------------------------------------------------------
/views/plotpage.utml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= partial("plot") %>
5 |
6 |
--------------------------------------------------------------------------------
/notifications/rotten.utml:
--------------------------------------------------------------------------------
1 | The <%- crop.name %> you planted
2 | is dead due to dehydration. What a shame. Next
3 | time, make sure to water your crop!
4 |
--------------------------------------------------------------------------------
/notifications/dehydration.utml:
--------------------------------------------------------------------------------
1 | The <%- crop.name %> you planted
2 | is dead due to dehydration. What a shame. Next
3 | time, make sure to water your crop!
4 |
--------------------------------------------------------------------------------
/views/plant.utml:
--------------------------------------------------------------------------------
1 | Plant a crop
2 |
3 |
4 |
5 | <% _.each(types, function(type) { %>
6 |
7 | <%= partial("croptype", {type: type}) %>
8 |
9 | <% }); %>
10 |
11 |
12 |
--------------------------------------------------------------------------------
/views/about.utml:
--------------------------------------------------------------------------------
1 | Open Farm Game is a game where you get to pretend you're a
2 | farmer. You plant crops and tend them and then those crops grow and
3 | then you get some more money to plant more crops.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/views/buy-plot.utml:
--------------------------------------------------------------------------------
1 | A new plot will cost 50 coins.
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/notifications/emptyplot.utml:
--------------------------------------------------------------------------------
1 | You've got an empty plot of
2 | land . You should plant a crop on it!
3 |
4 | It's only by growing crops and harvesting them that you get money
5 | to buy stuff, like... more land!
6 |
7 |
--------------------------------------------------------------------------------
/views/water.utml:
--------------------------------------------------------------------------------
1 | Watering your <%- crop.name %> will cost 1 coin.
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/views/croppage.utml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/notifications/ripecrop.utml:
--------------------------------------------------------------------------------
1 | The <%- crop.name %> you planted is
2 | ripe and ready to be harvested. That will get you some coins!
3 |
4 | Don't wait too long, or it will get overripe ;
5 | overripe crops are worth less money.
6 |
--------------------------------------------------------------------------------
/views/tearup.utml:
--------------------------------------------------------------------------------
1 | Are you sure you want to tear up your <%- crop.name %>? You will get no money for it.
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## generic files to ignore
2 | *~
3 | *.lock
4 | *.DS_Store
5 | *.swp
6 | *.out
7 |
8 | ## For node projects
9 | lib-cov
10 | *.seed
11 | *.log
12 | *.csv
13 | *.dat
14 | *.out
15 | *.pid
16 | *.gz
17 |
18 | pids
19 | logs
20 | results
21 |
22 | node_modules
23 | npm-debug.log
24 | config.js
25 |
--------------------------------------------------------------------------------
/notifications/reallyneedswater.utml:
--------------------------------------------------------------------------------
1 | The <%- crop.name %> you planted
2 | is parched . The crop has been damaged by neglect and
3 | won't earn you as many coins at harvest time.
4 |
5 | You should water it now; parched crops will
6 | eventually die .
7 |
--------------------------------------------------------------------------------
/views/harvest.utml:
--------------------------------------------------------------------------------
1 | Harvesting your <%- crop.name %> will earn you <%- (crop.damaged) ? Math.floor(type.price/2.0) : type.price %> coins.
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/notifications/needswater.utml:
--------------------------------------------------------------------------------
1 | The <%- crop.name %> you
2 | planted needs water . Your crop can't grow
3 | unless you water it!
4 |
5 | Don't wait too long, or it will get parched ;
6 | parched crops are worth less money when you eventually harvest
7 | them.
8 |
--------------------------------------------------------------------------------
/notifications/overripe.utml:
--------------------------------------------------------------------------------
1 | The <%- crop.name %> you planted
2 | is overripe . The crop has been damaged by neglect
3 | and won't earn you as many coins at harvest time.
4 |
5 | You should harvest it now; overripe crops will
6 | eventually rot in the fields . Gross!
7 |
--------------------------------------------------------------------------------
/views/login.utml:
--------------------------------------------------------------------------------
1 | Login
2 |
3 |
9 |
10 | If you don't have a pump handle yet, head on over to pump.io and you can get one for free!.
11 |
--------------------------------------------------------------------------------
/views/plot.utml:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% if (crop) { %>
4 | <%= partial("crop") %>
5 | <% } else { %>
6 |
7 |
(Empty)
8 |
This is an empty plot.
9 | <% if (user && user.id == farmer.id) { %>
10 |
Plant
11 | <% } %>
12 | <% } %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openfarmgame",
3 | "version": "0.1.0-alpha.1",
4 | "dependencies": {
5 | "express": "2.5.x",
6 | "utml": "0.2.x",
7 | "underscore": "1.4.x",
8 | "webfinger": "0.1.x",
9 | "databank": "~0.18.1",
10 | "underscore": "1.4.x",
11 | "dialback-client": "~0.1.1",
12 | "oauth": "0.9.x",
13 | "async": "0.2.x",
14 | "connect-databank": "0.8.x",
15 | "node-uuid": "1.4.x",
16 | "bunyan": "0.16.x"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/views/index.utml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Open Farm Game
4 |
5 |
Build your agricultural empire on the federated social
6 | web!
7 |
8 |
OFG
9 | the social
10 | network game where you run a farm, plant and care for crops, and
11 | use the proceeds to buy more land and better equipment. Show your friends
12 | who's the farm boss!
13 |
14 |
Get started »
15 |
16 |
17 |
--------------------------------------------------------------------------------
/views/nav-anonymous.utml:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/views/croptype.utml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
<%- type.name %>
5 |
Cost: <%- type.cost %> coins
6 |
7 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/views/nav-loggedin.utml:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/views/farmer.utml:
--------------------------------------------------------------------------------
1 | <%- farmer.name %> (<%- farmer.id %>)
2 |
3 | <%- farmer.coins %> coins
4 |
5 | Plots
6 |
7 |
8 | <% _.each(plots, function(plot) {
9 | var crop = (plot.crop) ? crops[plot.crop] : null; %>
10 | <%= partial("plot", {plot: plot, crop: crop}) %>
11 | <% }); %>
12 |
13 |
14 | <% if (user && user.id == farmer.id) { %>
15 | disabled="disabled" onclick="return false;"<% } %>>Buy a new plot
16 | <% } %>
17 |
--------------------------------------------------------------------------------
/views/crop.utml:
--------------------------------------------------------------------------------
1 |
2 | <%- crop.name %>
3 | <%- crop.status() %>
4 | <% if (user && user.id == farmer.id) { %>
5 |
6 | <% if (crop.needsWater()) { %>
7 | Water
8 | <% } %>
9 | <% if (crop.ready()) { %>
10 | Harvest
11 | <% } %>
12 | Tear up
13 |
14 | <% } %>
15 |
--------------------------------------------------------------------------------
/models/openfarmgame.js:
--------------------------------------------------------------------------------
1 | // openfarmgame.js
2 | //
3 | // data object representing the game itself
4 | //
5 | // Copyright 2013, StatusNet Inc.
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | var OpenFarmGame = {
20 |
21 | name: null,
22 |
23 | hostname: null,
24 |
25 | description: null,
26 |
27 | protocol: "http",
28 |
29 | url: function(rel) {
30 | var game = this;
31 | return game.protocol + "://" + game.hostname + rel;
32 | },
33 |
34 | asService: function() {
35 |
36 | var game = this;
37 |
38 | return {
39 | objectType: "service", // XXX: "game"?
40 | displayName: game.name,
41 | id: game.url("/"),
42 | url: game.url("/"),
43 | description: game.description
44 | };
45 | }
46 | };
47 |
48 | module.exports = OpenFarmGame;
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Open Farm Game
2 |
3 | This is a social networking game where you plant some stuff and then
4 | it grows and then you plant more stuff.
5 |
6 | That's about it for the current version. I'd love to make it more
7 | interesting.
8 |
9 | # File sources
10 |
11 | I mostly just used Google Image search with the public domain filter
12 | on. I think a lot of them came from other places.
13 |
14 | * dirt.jpg: http://www.public-domain-image.com/full-image/textures-and-patterns-public-domain-images-pictures/dirt-texture.jpg-royalty-free-stock-photograph.html
15 | * corn.jpg: http://pixabay.com/en/corn-corn-on-the-cob-corn-kernels-61771/
16 | * beans.jpg: http://en.wikipedia.org/wiki/File:Broad-beans-after-cooking.jpg
17 | * tomatoes.jpg: http://pixabay.com/en/tomatoes-vegetable-food-red-tasty-2042/
18 | * strawberries.jpg: http://pixabay.com/en/strawberry-fruits-fruit-51609/
19 | * wheat.jpg: http://pixabay.com/en/wheat-spike-cereal-grain-field-8844/
20 | * beets.jpg: http://www.flickr.com/photos/adactio/4701228386/ (small world)
21 | * pumpkins.jpg: http://www.flickr.com/photos/oatsy40/6299968619/
22 | * potatoes.jpg: http://pixabay.com/en/potatoes-vegetables-raw-uncooked-3455/
23 | * bellpeppers.jpg: http://www.freestockphotos.biz/stockphoto/9050
24 | * onions.jpg: http://www.public-domain-image.com/flora-plants-public-domain-images-pictures/vegetables-public-domain-images-pictures/onion-pictures/yellow-onions-vegetables.jpg.html
25 | * chilipeppers.jpg: http://www.public-domain-image.com/flora-plants-public-domain-images-pictures/vegetables-public-domain-images-pictures/pepper-pictures/chili-chillies-peppers.jpg.html
26 |
--------------------------------------------------------------------------------
/models/requesttoken.js:
--------------------------------------------------------------------------------
1 | // requesttoken.js
2 | //
3 | // data object representing a requesttoken
4 | //
5 | // Copyright 2013, StatusNet Inc.
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | var _ = require("underscore"),
20 | DatabankObject = require("databank").DatabankObject;
21 |
22 | var RequestToken = DatabankObject.subClass("requesttoken");
23 |
24 | RequestToken.schema = {
25 | pkey: "hostname_token",
26 | fields: ["hostname",
27 | "token",
28 | "secret"]
29 | };
30 |
31 | RequestToken.key = function(hostname, token) {
32 | return hostname + "/" + token;
33 | };
34 |
35 | RequestToken.beforeCreate = function(props, callback) {
36 |
37 | var i, required = ["hostname", "token", "secret"],
38 | fail = false;
39 |
40 | for (i = 0; i < required.length; i++) {
41 | if (!_.has(props, required[i])) {
42 | callback(new Error("Missing required property: " + required[i]), null);
43 | return;
44 | }
45 | }
46 |
47 | if (!_.has(props, "hostname_token")) {
48 | props.hostname_token = RequestToken.key(props.hostname, props.token);
49 | }
50 |
51 | callback(null, props);
52 | };
53 |
54 | module.exports = RequestToken;
55 |
--------------------------------------------------------------------------------
/views/layout.utml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Open Farm Game
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | <% if (user) { %>
31 | <%= partial("nav-loggedin") %>
32 | <% } else { %>
33 | <%= partial("nav-anonymous") %>
34 | <% } %>
35 |
36 |
37 |
38 |
39 |
40 |
41 | <%= body %>
42 |
43 |
44 |
45 |
46 |
47 | OpenFarmGame is brought to you by E14N , because we think you're awesome.
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/models/plot.js:
--------------------------------------------------------------------------------
1 | // plot.js
2 | //
3 | // data object representing a plot of land
4 | //
5 | // Copyright 2013, StatusNet Inc.
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | var _ = require("underscore"),
20 | async = require("async"),
21 | uuid = require("node-uuid"),
22 | DatabankObject = require("databank").DatabankObject,
23 | OpenFarmGame = require("./openfarmgame"),
24 | Host = require("./host");
25 |
26 | var Plot = DatabankObject.subClass("plot");
27 |
28 | Plot.schema = {
29 | pkey: "uuid",
30 | fields: ["owner",
31 | "crop",
32 | "created",
33 | "updated"]
34 | };
35 |
36 | Plot.beforeCreate = function(props, callback) {
37 | props.uuid = uuid.v4();
38 | props.created = Date.now();
39 | props.updated = props.created;
40 | callback(null, props);
41 | };
42 |
43 | Plot.prototype.beforeUpdate = function(props, callback) {
44 | props.updated = Date.now();
45 | callback(null, props);
46 | };
47 |
48 | Plot.prototype.beforeSave = function(callback) {
49 | var plot = this;
50 | plot.updated = Date.now();
51 | if (!plot.created) {
52 | plot.created = Date.now();
53 | }
54 | if (!plot.uuid) {
55 | plot.uuid = uuid.v4();
56 | }
57 | callback(null);
58 | };
59 |
60 | Plot.prototype.url = function(callback) {
61 | var plot = this;
62 | return OpenFarmGame.url("/plot/"+plot.uuid);
63 | };
64 |
65 | Plot.prototype.asObject = function() {
66 | var plot = this;
67 | return {
68 | id: "urn:uuid:"+plot.uuid,
69 | objectType: "http://openfarmgame.com/schema/object-type/plot",
70 | displayName: "a plot of land",
71 | url: plot.url()
72 | };
73 | };
74 |
75 | module.exports = Plot;
76 |
--------------------------------------------------------------------------------
/data/croptypes.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "slug": "corn",
4 | "name": "Corn",
5 | "cost": 5,
6 | "price": 17,
7 | "waterings": 3,
8 | "watertime": 120,
9 | "reallywatertime": 1440,
10 | "dehydrationtime": 1440,
11 | "ripentime": 120,
12 | "overripentime": 1440,
13 | "rottime": 1440
14 | },
15 | {
16 | "slug": "tomatoes",
17 | "name": "Tomatoes",
18 | "cost": 3,
19 | "price": 14,
20 | "waterings": 5,
21 | "watertime": 60,
22 | "reallywatertime": 720,
23 | "dehydrationtime": 720,
24 | "ripentime": 60,
25 | "overripentime": 720,
26 | "rottime": 720
27 | },
28 | {
29 | "slug": "beans",
30 | "name": "Beans",
31 | "cost": 4,
32 | "price": 12,
33 | "waterings": 2,
34 | "watertime": 240,
35 | "reallywatertime": 1440,
36 | "dehydrationtime": 1440,
37 | "ripentime": 240,
38 | "overripentime": 720,
39 | "rottime": 720
40 | },
41 | {
42 | "slug": "strawberries",
43 | "name": "Strawberries",
44 | "cost": 6,
45 | "price": 25,
46 | "waterings": 4,
47 | "watertime": 120,
48 | "reallywatertime": 1440,
49 | "dehydrationtime": 1440,
50 | "ripentime": 60,
51 | "overripentime": 720,
52 | "rottime": 720
53 | },
54 | {
55 | "slug": "wheat",
56 | "name": "Wheat",
57 | "cost": 6,
58 | "price": 16,
59 | "waterings": 2,
60 | "watertime": 120,
61 | "reallywatertime": 1440,
62 | "dehydrationtime": 1440,
63 | "ripentime": 240,
64 | "overripentime": 720,
65 | "rottime": 360
66 | },
67 | {
68 | "slug": "beets",
69 | "name": "Beets",
70 | "cost": 2,
71 | "price": 8,
72 | "waterings": 4,
73 | "watertime": 240,
74 | "reallywatertime": 720,
75 | "dehydrationtime": 720,
76 | "ripentime": 120,
77 | "overripentime": 1440,
78 | "rottime": 1440
79 | },
80 | {
81 | "slug": "pumpkins",
82 | "name": "Pumpkins",
83 | "cost": 3,
84 | "price": 11,
85 | "waterings": 3,
86 | "watertime": 120,
87 | "reallywatertime": 720,
88 | "dehydrationtime": 720,
89 | "ripentime": 120,
90 | "overripentime": 1440,
91 | "rottime": 1440
92 | },
93 | {
94 | "slug": "potatoes",
95 | "name": "Potatoes",
96 | "cost": 5,
97 | "price": 22,
98 | "waterings": 5,
99 | "watertime": 120,
100 | "reallywatertime": 360,
101 | "dehydrationtime": 360,
102 | "ripentime": 120,
103 | "overripentime": 720,
104 | "rottime": 720
105 | },
106 | {
107 | "slug": "bellpeppers",
108 | "name": "Bell Peppers",
109 | "cost": 4,
110 | "price": 23,
111 | "waterings": 5,
112 | "watertime": 120,
113 | "reallywatertime": 720,
114 | "dehydrationtime": 720,
115 | "ripentime": 120,
116 | "overripentime": 720,
117 | "rottime": 720
118 | },
119 | {
120 | "slug": "onions",
121 | "name": "Onions",
122 | "cost": 3,
123 | "price": 12,
124 | "waterings": 6,
125 | "watertime": 360,
126 | "reallywatertime": 1440,
127 | "dehydrationtime": 1440,
128 | "ripentime": 360,
129 | "overripentime": 1440,
130 | "rottime": 1440
131 | },
132 | {
133 | "slug": "chilipeppers",
134 | "name": "Chili Peppers",
135 | "cost": 5,
136 | "price": 26,
137 | "waterings": 5,
138 | "watertime": 120,
139 | "reallywatertime": 720,
140 | "dehydrationtime": 720,
141 | "ripentime": 120,
142 | "overripentime": 360,
143 | "rottime": 360
144 | }
145 | ]
146 |
--------------------------------------------------------------------------------
/models/crop.js:
--------------------------------------------------------------------------------
1 | // crop.js
2 | //
3 | // data object representing a crop planted in a plot
4 | //
5 | // Copyright 2013, StatusNet Inc.
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | var _ = require("underscore"),
20 | async = require("async"),
21 | uuid = require("node-uuid"),
22 | OpenFarmGame = require("./openfarmgame"),
23 | DatabankObject = require("databank").DatabankObject;
24 |
25 | var Crop = DatabankObject.subClass("crop");
26 |
27 | Crop.schema = {
28 | pkey: "uuid",
29 | fields: ["owner",
30 | "plot",
31 | "type",
32 | "state",
33 | "watered",
34 | "created",
35 | "updated"]
36 | };
37 |
38 | Crop.beforeCreate = function(props, callback) {
39 | props.uuid = uuid.v4();
40 | props.created = Date.now();
41 | props.updated = props.created;
42 | if (!props.watered) {
43 | props.watered = 0;
44 | }
45 | if (!props.state) {
46 | props.state = Crop.NEW;
47 | }
48 | console.dir(props);
49 | callback(null, props);
50 | };
51 |
52 | Crop.prototype.beforeUpdate = function(props, callback) {
53 | props.updated = Date.now();
54 | callback(null, props);
55 | };
56 |
57 | Crop.prototype.beforeSave = function(callback) {
58 | var crop = this;
59 | crop.updated = Date.now();
60 | if (!crop.created) {
61 | crop.created = Date.now();
62 | }
63 | if (!crop.uuid) {
64 | crop.uuid = uuid.v4();
65 | if (!crop.watered) {
66 | crop.watered = 0;
67 | }
68 | if (!crop.state) {
69 | crop.state = Crop.NEW;
70 | }
71 | }
72 | callback(null);
73 | };
74 |
75 | Crop.prototype.url = function() {
76 | var crop = this;
77 | return OpenFarmGame.url("/crop/"+crop.uuid);
78 | };
79 |
80 | Crop.prototype.asObject = function() {
81 | var crop = this;
82 | return {
83 | id: "urn:uuid:"+crop.uuid,
84 | objectType: "http://openfarmgame.com/schema/object-type/crop",
85 | url: crop.url(),
86 | displayName: crop.name
87 | };
88 | };
89 |
90 | Crop.prototype.status = function() {
91 | var crop = this,
92 | status;
93 |
94 | switch (crop.state) {
95 | case Crop.NEW:
96 | status = "New";
97 | break;
98 | case Crop.GROWING:
99 | status = "Growing";
100 | break;
101 | case Crop.NEEDS_WATER:
102 | status = "Needs water";
103 | break;
104 | case Crop.REALLY_NEEDS_WATER:
105 | status = "Really needs water";
106 | break;
107 | case Crop.RIPE:
108 | status = "Ripe";
109 | break;
110 | case Crop.OVERRIPE:
111 | status = "Overripe";
112 | break;
113 | case Crop.DEAD:
114 | status = "Dead";
115 | break;
116 | case Crop.HARVESTED:
117 | status = "Harvested";
118 | break;
119 | default:
120 | status = "(Unrecognized state '" + crop.state + "')";
121 | break;
122 | }
123 |
124 | return status;
125 | };
126 |
127 | Crop.prototype.needsWater = function() {
128 | var crop = this;
129 | return (crop.state == Crop.NEW ||
130 | crop.state == Crop.NEEDS_WATER ||
131 | crop.state == Crop.REALLY_NEEDS_WATER);
132 | };
133 |
134 | Crop.prototype.ready = function() {
135 | var crop = this;
136 | return (crop.state == Crop.RIPE ||
137 | crop.state == Crop.OVERRIPE);
138 | };
139 |
140 | // States
141 |
142 | Crop.NEW = "new";
143 | Crop.GROWING = "growing";
144 | Crop.NEEDS_WATER = "needswater";
145 | Crop.REALLY_NEEDS_WATER = "reallyneedswater";
146 | Crop.RIPE = "ripe";
147 | Crop.OVERRIPE = "overripe";
148 | Crop.DEAD = "dead";
149 | Crop.HARVESTED = "harvested";
150 |
151 | module.exports = Crop;
152 |
--------------------------------------------------------------------------------
/models/croptype.js:
--------------------------------------------------------------------------------
1 | // croptype.js
2 | //
3 | // data object representing a type of plant used in farming
4 | //
5 | // Copyright 2013, StatusNet Inc.
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | var _ = require("underscore"),
20 | async = require("async"),
21 | fs = require("fs"),
22 | path = require("path"),
23 | DatabankObject = require("databank").DatabankObject;
24 |
25 | var CropType = DatabankObject.subClass("croptype");
26 |
27 | CropType.schema = {
28 | "croptype": {
29 | pkey: "slug",
30 | fields: ["name",
31 | "cost",
32 | "price",
33 | "waterings",
34 | "watertime",
35 | "reallywatertime",
36 | "dehydrationtime",
37 | "ripentime",
38 | "overripentime",
39 | "rottime",
40 | "created",
41 | "updated"]
42 | },
43 | "croptypelist": {
44 | pkey: "dummy"
45 | }
46 | };
47 |
48 | CropType.beforeCreate = function(props, callback) {
49 | if (!props.slug) {
50 | callback(new Error("No slug!"), null);
51 | return;
52 | }
53 | props.created = Date.now();
54 | props.updated = props.created;
55 | callback(null, props);
56 | };
57 |
58 | CropType.prototype.afterCreate = function(callback) {
59 | var type = this;
60 | var bank = CropType.bank();
61 | bank.append("croptypelist", 0, type.slug, function(err, list) {
62 | callback(err);
63 | });
64 | };
65 |
66 | CropType.prototype.beforeUpdate = function(props, callback) {
67 | if (!props.slug) {
68 | callback(new Error("No slug!"), null);
69 | return;
70 | }
71 | props.updated = Date.now();
72 | callback(null, props);
73 | };
74 |
75 | CropType.prototype.beforeSave = function(callback) {
76 | var type = this;
77 | if (!type.slug) {
78 | callback(new Error("No slug!"));
79 | return;
80 | }
81 | type.updated = Date.now();
82 | if (!type.created) {
83 | type.created = Date.now();
84 | }
85 | callback(null);
86 | };
87 |
88 | CropType.prototype.afterSave = function(callback) {
89 | var type = this;
90 | var bank = CropType.bank();
91 | bank.indexOf("croptypelist", 0, type.slug, function(err, index) {
92 | if ((err && err.name == "NoSuchThingError") ||
93 | index === -1) {
94 | bank.append("croptypelist", 0, type.slug, callback);
95 | } else {
96 | callback(null);
97 | }
98 | });
99 | };
100 |
101 | CropType.getAll = function(callback) {
102 | var bank = CropType.bank();
103 |
104 | async.waterfall([
105 | function(callback) {
106 | bank.read("croptypelist", 0, callback);
107 | },
108 | function(slugs, callback) {
109 | CropType.readArray(slugs, callback);
110 | }
111 | ], function(err, crops) {
112 | if (err) {
113 | callback(err, null);
114 | } else {
115 | _.sortBy(crops, "cost");
116 | callback(null, crops);
117 | }
118 | });
119 | };
120 |
121 | CropType.initialData = function(callback) {
122 | var fname = path.join(__dirname, "..", "data", "croptypes.json");
123 |
124 | async.waterfall([
125 | function(callback) {
126 | fs.readFile(fname, "utf8", callback);
127 | },
128 | function(data, callback) {
129 | var arr;
130 | try {
131 | arr = JSON.parse(data);
132 | callback(null, arr);
133 | } catch(e) {
134 | callback(e, null);
135 | }
136 | },
137 | function(specs, callback) {
138 | async.forEach(specs,
139 | function(spec, callback) {
140 | var type = new CropType(spec);
141 | type.save(callback);
142 | },
143 | callback);
144 | }
145 | ], function(err, results) {
146 | callback(err);
147 | });
148 | };
149 |
150 | module.exports = CropType;
151 |
--------------------------------------------------------------------------------
/lib/notifier.js:
--------------------------------------------------------------------------------
1 | // notifier.js
2 | //
3 | // Notifies farmers of changes in the state of the game
4 | //
5 | // Copyright 2013, StatusNet Inc.
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | var _ = require("underscore"),
20 | fs = require("fs"),
21 | path = require("path"),
22 | async = require("async"),
23 | uuid = require("node-uuid"),
24 | Farmer = require("../models/farmer"),
25 | Host = require("../models/host"),
26 | OpenFarmGame = require("../models/openfarmgame");
27 |
28 | var Notifier = function(options) {
29 |
30 | // Private methods
31 |
32 | var templates = {},
33 | getTemplate = function(template, callback) {
34 | if (_.has(templates, template)) {
35 | callback(null, templates[template]);
36 | } else {
37 | compileTemplate(template, callback);
38 | }
39 | },
40 | compileTemplate = function(template, callback) {
41 | async.waterfall([
42 | function(callback) {
43 | var fname = path.join(__dirname, "..", "notifications", template + ".utml");
44 | fs.readFile(fname, "utf8", callback);
45 | },
46 | function(contents, callback) {
47 | var fn = _.template(contents);
48 | templates[template] = fn;
49 | callback(null, fn);
50 | }
51 | ], callback);
52 | },
53 | renderTemplate = function(template, data, callback) {
54 | getTemplate(template, function(err, fn) {
55 | var html;
56 |
57 | if (err) {
58 | callback(err, null);
59 | } else {
60 | try {
61 | html = fn(data);
62 | callback(null, html);
63 | } catch(e) {
64 | callback(e, null);
65 | }
66 | }
67 | });
68 | },
69 | sendNote = function(farmer, title, content, callback) {
70 | async.waterfall([
71 | function(callback) {
72 | farmer.getHost(callback);
73 | },
74 | function(host, callback) {
75 | var oa = host.getOAuth(),
76 | now = new Date(),
77 | act = {
78 | id: "urn:uuid:"+uuid.v4(),
79 | actor: OpenFarmGame.asService(),
80 | verb: "post",
81 | to: [{id: "acct:" + farmer.id,
82 | objectType: "person"}],
83 | object: {
84 | id: "urn:uuid:"+uuid.v4(),
85 | objectType: "note",
86 | displayName: title,
87 | content: content,
88 | published: now.toISOString()
89 | },
90 | published: now.toISOString()
91 | };
92 |
93 | oa.post(farmer.inbox, null, null, JSON.stringify(act), "application/json", callback);
94 | }
95 | ], function(err, data, response) {
96 | if (err) {
97 | callback(err);
98 | } else if (response.statusCode >= 400 && response.statusCode < 600) {
99 | callback(new Error("HTTP error " + response.statusCode + ": " + data));
100 | } else {
101 | callback(null);
102 | }
103 | });
104 | };
105 |
106 | // Initialize contents
107 |
108 | this.options = options || {};
109 |
110 | // Privileged method
111 |
112 | this.notify = function(farmer, title, template, data, callback) {
113 | var notifier = this;
114 |
115 | async.waterfall([
116 | function(callback) {
117 | var ext = _.clone(data);
118 |
119 | ext.game = OpenFarmGame.asService();
120 | ext.farmer = farmer;
121 |
122 | renderTemplate(template, ext, callback);
123 | },
124 | function(contents, callback) {
125 | sendNote(farmer, title, contents, callback);
126 | }
127 | ], callback);
128 | };
129 | };
130 |
131 | module.exports = Notifier;
132 |
--------------------------------------------------------------------------------
/models/host.js:
--------------------------------------------------------------------------------
1 | // host.js
2 | //
3 | // data object representing a remote host
4 | //
5 | // Copyright 2013, StatusNet Inc.
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | var _ = require("underscore"),
20 | wf = require("webfinger"),
21 | async = require("async"),
22 | qs = require("querystring"),
23 | OAuth = require("oauth").OAuth,
24 | DatabankObject = require("databank").DatabankObject,
25 | OpenFarmGame = require("./openfarmgame"),
26 | RequestToken = require("./requesttoken");
27 |
28 | var Host = DatabankObject.subClass("host");
29 |
30 | var OAUTH_RT = "http://apinamespace.org/oauth/request_token",
31 | OAUTH_AT = "http://apinamespace.org/oauth/access_token",
32 | OAUTH_AUTHZ = "http://apinamespace.org/oauth/authorize",
33 | WHOAMI = "http://apinamespace.org/activitypub/whoami",
34 | OAUTH_CRED = "registration_endpoint";
35 |
36 | Host.schema = {
37 | pkey: "hostname",
38 | fields: ["client_id",
39 | "client_secret",
40 | "registration_endpoint",
41 | "request_token_endpoint",
42 | "access_token_endpoint",
43 | "authorization_endpoint",
44 | "whoami_endpoint",
45 | "created",
46 | "updated"]
47 | };
48 |
49 | Host.ensureHost = function(hostname, callback) {
50 | Host.get(hostname, function(err, host) {
51 | if (err && err.name == "NoSuchThingError") {
52 | Host.discover(hostname, callback);
53 | } else if (err) {
54 | callback(err, null);
55 | } else {
56 | // XXX: update endpoints?
57 | callback(null, host);
58 | }
59 | });
60 | };
61 |
62 | Host.discover = function(hostname, callback) {
63 |
64 | var props = {
65 | hostname: hostname
66 | };
67 |
68 | async.waterfall([
69 | function(callback) {
70 | wf.hostmeta(hostname, callback);
71 | },
72 | function(jrd, callback) {
73 | var rels = {
74 | registration_endpoint: OAUTH_CRED,
75 | request_token_endpoint: OAUTH_RT,
76 | access_token_endpoint: OAUTH_AT,
77 | authorization_endpoint: OAUTH_AUTHZ,
78 | whoami_endpoint: WHOAMI
79 | },
80 | prop,
81 | rel;
82 |
83 | for (prop in rels) {
84 | rel = rels[prop];
85 | var links = _.where(jrd.links, {rel: rel});
86 | if (links.length === 0) {
87 | callback(new Error(hostname + " does not implement " + rel), null);
88 | return;
89 | } else {
90 | props[prop] = links[0].href;
91 | }
92 | }
93 |
94 | Host.getCredentials(props.registration_endpoint, callback);
95 | },
96 | function(cred, callback) {
97 | props.client_id = cred.client_id;
98 | props.client_secret = cred.client_secret;
99 | Host.create(props, callback);
100 | }
101 | ], callback);
102 | };
103 |
104 | Host.getCredentials = function(endpoint, callback) {
105 | async.waterfall([
106 | function(callback) {
107 | var body = qs.stringify({type: "client_associate",
108 | application_type: "web",
109 | application_name: "Open Farm Game",
110 | redirect_uris: OpenFarmGame.url("/authorized")});
111 |
112 | Host.dialbackClient.post(endpoint,
113 | OpenFarmGame.hostname,
114 | body,
115 | "application/x-www-form-urlencoded",
116 | callback);
117 | }
118 | ], function(err, response, doc) {
119 | var client;
120 | if (err) {
121 | callback(err, null);
122 | } else if (response.statusCode >= 400 && response.statusCode < 600) {
123 | callback(new Error("HTTP Error " + response.statusCode + ": " + doc), null);
124 | } else if (!response.headers["content-type"]) {
125 | callback(new Error("No content type"), null);
126 | } else if (response.headers["content-type"].substr(0, "application/json".length) != "application/json") {
127 | callback(new Error("Bad content type: " + response.headers["content-type"]), null);
128 | } else {
129 | try {
130 | client = JSON.parse(doc);
131 | callback(null, client);
132 | } catch (e) {
133 | callback(e, null);
134 | }
135 | }
136 | });
137 | };
138 |
139 | Host.prototype.getRequestToken = function(callback) {
140 | var host = this,
141 | oa = host.getOAuth();
142 |
143 | async.waterfall([
144 | function(callback) {
145 | oa.getOAuthRequestToken(callback);
146 | },
147 | function(token, secret, other, callback) {
148 | RequestToken.create({token: token,
149 | secret: secret,
150 | hostname: host.hostname},
151 | callback);
152 | }
153 | ], callback);
154 | };
155 |
156 | Host.prototype.authorizeURL = function(rt, callback) {
157 | var host = this,
158 | separator;
159 |
160 | if (_.contains(host.authorization_endpoint, "?")) {
161 | separator = "&";
162 | } else {
163 | separator = "?";
164 | }
165 |
166 | return host.authorization_endpoint + separator + "oauth_token=" + rt.token;
167 | };
168 |
169 | Host.prototype.getAccessToken = function(rt, verifier, callback) {
170 | var host = this,
171 | oa = host.getOAuth();
172 |
173 | oa.getOAuthAccessToken(rt.token, rt.secret, verifier, callback);
174 | };
175 |
176 | Host.prototype.whoami = function(token, secret, callback) {
177 | var host = this,
178 | oa = host.getOAuth();
179 |
180 | // XXX: ssl
181 |
182 | async.waterfall([
183 | function(callback) {
184 | oa.get(host.whoami_endpoint, token, secret, callback);
185 | }
186 | ], function(err, doc, response) {
187 | var obj;
188 | if (err) {
189 | callback(err, null);
190 | } else {
191 | try {
192 | obj = JSON.parse(doc);
193 | callback(null, obj);
194 | } catch(e) {
195 | callback(e, null);
196 | }
197 | }
198 | });
199 | };
200 |
201 | Host.prototype.getOAuth = function() {
202 |
203 | var host = this;
204 |
205 | return new OAuth(host.request_token_endpoint,
206 | host.access_token_endpoint,
207 | host.client_id,
208 | host.client_secret,
209 | "1.0",
210 | OpenFarmGame.url("/authorized/"+host.hostname),
211 | "HMAC-SHA1",
212 | null, // nonce size; use default
213 | {"User-Agent": "openfarmgame.com/0.1.0"});
214 | };
215 |
216 | module.exports = Host;
217 |
--------------------------------------------------------------------------------
/models/farmer.js:
--------------------------------------------------------------------------------
1 | // farmer.js
2 | //
3 | // data object representing an farmer
4 | //
5 | // Copyright 2013, StatusNet Inc.
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | var _ = require("underscore"),
20 | async = require("async"),
21 | uuid = require("node-uuid"),
22 | DatabankObject = require("databank").DatabankObject,
23 | OpenFarmGame = require("./openfarmgame"),
24 | Host = require("./host"),
25 | Plot = require("./plot");
26 |
27 | var Farmer = DatabankObject.subClass("farmer");
28 |
29 | Farmer.schema = {
30 | "farmer": {
31 | pkey: "id",
32 | fields: ["name",
33 | "coins",
34 | "plots",
35 | "token",
36 | "secret",
37 | "inbox",
38 | "outbox",
39 | "created",
40 | "updated"]
41 | },
42 | "farmerlist": {
43 | pkey: "id"
44 | }
45 | };
46 |
47 | Farmer.fromPerson = function(person, token, secret, callback) {
48 |
49 | var id = person.id,
50 | farmer,
51 | bank = Farmer.bank();
52 |
53 | if (id.substr(0, 5) == "acct:") {
54 | id = id.substr(5);
55 | }
56 |
57 | if (!person.links ||
58 | !person.links["activity-inbox"] ||
59 | !person.links["activity-inbox"].href) {
60 | callback(new Error("No activity inbox."));
61 | return;
62 | }
63 |
64 | if (!person.links ||
65 | !person.links["activity-outbox"] ||
66 | !person.links["activity-outbox"].href) {
67 | callback(new Error("No activity inbox."));
68 | return;
69 | }
70 |
71 | if (!person.followers ||
72 | !person.followers.url) {
73 | callback(new Error("No followers."));
74 | return;
75 | }
76 |
77 | async.waterfall([
78 | function(callback) {
79 | Plot.create({owner: id}, callback);
80 | },
81 | function(plot, callback) {
82 | Farmer.create({id: id,
83 | name: person.displayName,
84 | homepage: person.url,
85 | coins: 25,
86 | plots: [plot.uuid],
87 | token: token,
88 | secret: secret,
89 | created: Date.now(),
90 | updated: Date.now(),
91 | inbox: person.links["activity-inbox"].href,
92 | outbox: person.links["activity-outbox"].href,
93 | followers: person.followers.url},
94 | callback);
95 | }
96 | ], callback);
97 | };
98 |
99 | // Keep a list of existing farmers so we can do periodic updates
100 |
101 | Farmer.prototype.afterCreate = function(callback) {
102 | var farmer = this,
103 | bank = Farmer.bank();
104 |
105 | bank.append("farmerlist", 0, farmer.id, function(err, list) {
106 | if (err) {
107 | callback(err);
108 | } else {
109 | callback(null);
110 | }
111 | });
112 | };
113 |
114 | // Deleted farmers come off the list
115 |
116 | Farmer.prototype.afterDel = function(callback) {
117 | var farmer = this;
118 |
119 | async.parallel([
120 | function(callback) {
121 | var bank = Farmer.bank();
122 | bank.remove("farmerlist", 0, farmer.id, callback);
123 | },
124 | function(callback) {
125 | var bank = Plot.bank();
126 | async.forEach(farmer.plots,
127 | function(plotID, callback) {
128 | bank.del("plot", plotID, callback);
129 | },
130 | callback);
131 | }
132 | ], function(err, results) {
133 | if (err) {
134 | callback(err);
135 | } else {
136 | callback(null);
137 | }
138 | });
139 | };
140 |
141 | Farmer.prototype.joinActivity = function(callback) {
142 | var farmer = this,
143 | game = OpenFarmGame.asService(),
144 | content = "" + farmer.name + " " +
145 | " joined " +
146 | "" + game.displayName + " ";
147 |
148 | farmer.postActivity({verb: "join",
149 | content: content,
150 | object: game},
151 | callback);
152 | };
153 |
154 | Farmer.prototype.buyActivity = function(plot, callback) {
155 | var farmer = this,
156 | obj = plot.asObject(),
157 | content = "" + farmer.name + " " +
158 | " bought " +
159 | "a new plot ";
160 |
161 | farmer.postActivity({verb: "purchase",
162 | content: content,
163 | object: obj},
164 | callback);
165 | };
166 |
167 | Farmer.prototype.plantActivity = function(crop, callback) {
168 | var farmer = this,
169 | obj = crop.asObject(),
170 | content = "" + farmer.name + " " +
171 | " planted " +
172 | "" + obj.displayName + " ";
173 |
174 | farmer.postActivity({verb: "http://openfarmgame.com/schema/verb/plant",
175 | content: content,
176 | object: obj},
177 | callback);
178 | };
179 |
180 | Farmer.prototype.tearUpActivity = function(crop, callback) {
181 | var farmer = this,
182 | obj = crop.asObject(),
183 | content = "" + farmer.name + " " +
184 | " tore up a field of " +
185 | obj.displayName;
186 |
187 | farmer.postActivity({verb: "http://openfarmgame.com/schema/verb/tear-up",
188 | content: content,
189 | object: obj},
190 | callback);
191 | };
192 |
193 | Farmer.prototype.waterActivity = function(crop, callback) {
194 | var farmer = this,
195 | obj = crop.asObject(),
196 | content = "" + farmer.name + " " +
197 | " watered " +
198 | "" + obj.displayName + " ";
199 |
200 | farmer.postActivity({verb: "http://openfarmgame.com/schema/verb/water",
201 | content: content,
202 | object: obj},
203 | callback);
204 | };
205 |
206 | Farmer.prototype.harvestActivity = function(crop, callback) {
207 | var farmer = this,
208 | obj = crop.asObject(),
209 | content = "" + farmer.name + " " +
210 | " harvested " +
211 | "" + obj.displayName + " ";
212 |
213 | farmer.postActivity({verb: "http://openfarmgame.com/schema/verb/harvest",
214 | content: content,
215 | object: obj},
216 | callback);
217 | };
218 |
219 | Farmer.getHostname = function(id) {
220 | var parts = id.split("@"),
221 | hostname = parts[1].toLowerCase();
222 |
223 | return hostname;
224 | };
225 |
226 | Farmer.prototype.getHost = function(callback) {
227 |
228 | var farmer = this,
229 | hostname = Farmer.getHostname(farmer.id);
230 |
231 | Host.get(hostname, callback);
232 | };
233 |
234 | Farmer.prototype.postActivity = function(act, callback) {
235 |
236 | var farmer = this;
237 |
238 | async.waterfall([
239 | function(callback) {
240 | farmer.getHost(callback);
241 | },
242 | function(host, callback) {
243 | var oa = host.getOAuth(),
244 | json = JSON.stringify(act);
245 |
246 | oa.post(farmer.outbox, farmer.token, farmer.secret, json, "application/json", callback);
247 | },
248 | function(data, response, callback) {
249 | var posted;
250 | if (response.statusCode >= 400 && response.statusCode < 600) {
251 | callback(new Error("Error " + response.StatusCode + ": " + data));
252 | } else if (!response.headers ||
253 | !response.headers["content-type"] ||
254 | response.headers["content-type"].substr(0, "application/json".length) != "application/json") {
255 | callback(new Error("Not application/json"));
256 | } else {
257 | try {
258 | posted = JSON.parse(data);
259 | callback(null, posted);
260 | } catch (e) {
261 | callback(e, null);
262 | }
263 | }
264 | }
265 | ], callback);
266 | };
267 |
268 | module.exports = Farmer;
269 |
--------------------------------------------------------------------------------
/public/images/coin.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
13 |
15 |
19 |
23 |
27 |
28 |
30 |
34 |
38 |
39 |
41 |
45 |
49 |
50 |
58 |
66 |
75 |
76 |
80 |
84 |
88 |
93 |
94 |
97 |
101 |
102 |
105 |
109 |
110 |
113 |
117 |
121 |
122 |
125 |
130 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/Apache-2.0:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | // app.js
2 | //
3 | // main function for open farm game
4 | //
5 | // Copyright 2013, StatusNet Inc.
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | var fs = require("fs"),
20 | async = require("async"),
21 | path = require("path"),
22 | _ = require("underscore"),
23 | express = require('express'),
24 | DialbackClient = require("dialback-client"),
25 | Logger = require("bunyan"),
26 | routes = require('./routes'),
27 | databank = require("databank"),
28 | uuid = require("node-uuid"),
29 | Databank = databank.Databank,
30 | DatabankObject = databank.DatabankObject,
31 | DatabankStore = require('connect-databank')(express),
32 | RequestToken = require("./models/requesttoken"),
33 | Farmer = require("./models/farmer"),
34 | Host = require("./models/host"),
35 | Plot = require("./models/plot"),
36 | Crop = require("./models/crop"),
37 | CropType = require("./models/croptype"),
38 | OpenFarmGame = require("./models/openfarmgame"),
39 | Notifier = require("./lib/notifier"),
40 | Updater = require("./lib/updater"),
41 | config,
42 | defaults = {
43 | port: 4000,
44 | address: "localhost",
45 | hostname: "localhost",
46 | driver: "disk",
47 | name: "Open Farm Game",
48 | description: "The social game that brings the excitement of subsistence farming to the social internet."
49 | },
50 | log,
51 | logParams = {
52 | name: "openfarmgame",
53 | serializers: {
54 | req: Logger.stdSerializers.req,
55 | res: Logger.stdSerializers.res
56 | }
57 | };
58 |
59 | if (fs.existsSync("/etc/openfarmgame.json")) {
60 | config = _.defaults(JSON.parse(fs.readFileSync("/etc/openfarmgame.json")),
61 | defaults);
62 | } else {
63 | config = defaults;
64 | }
65 |
66 | if (config.logfile) {
67 | logParams.streams = [{path: config.logfile}];
68 | } else if (config.nologger) {
69 | logParams.streams = [{path: "/dev/null"}];
70 | } else {
71 | logParams.streams = [{stream: process.stderr}];
72 | }
73 |
74 | log = new Logger(logParams);
75 |
76 | log.info("Initializing pump.io");
77 |
78 | if (!config.params) {
79 | if (config.driver == "disk") {
80 | config.params = {dir: "/var/lib/openfarmgame/"};
81 | } else {
82 | config.params = {};
83 | }
84 | }
85 |
86 | // Define the database schema
87 |
88 | if (!config.params.schema) {
89 | config.params.schema = {};
90 | }
91 |
92 | _.extend(config.params.schema, DialbackClient.schema);
93 | _.extend(config.params.schema, DatabankStore.schema);
94 |
95 | // Now, our stuff
96 |
97 | _.each([RequestToken, Host, Plot, Crop], function(Cls) {
98 | config.params.schema[Cls.type] = Cls.schema;
99 | });
100 |
101 | // Farmer and CropType have global lists
102 |
103 | _.extend(config.params.schema, Farmer.schema);
104 | _.extend(config.params.schema, CropType.schema);
105 |
106 | var db = Databank.get(config.driver, config.params);
107 |
108 | async.waterfall([
109 | function(callback) {
110 | log.info({driver: config.driver, params: config.params}, "Connecting to DB");
111 | db.connect({}, callback);
112 | },
113 | function(callback) {
114 |
115 | // Set global databank info
116 |
117 | DatabankObject.bank = db;
118 |
119 | // Set initial croptype data
120 |
121 | log.info("Setting initial crop data");
122 |
123 | CropType.initialData(callback);
124 | },
125 | function(callback) {
126 |
127 | var app,
128 | bounce,
129 | client,
130 | requestLogger = function(log) {
131 | return function(req, res, next) {
132 | var weblog = log.child({"req_id": uuid.v4(), component: "web"});
133 | var end = res.end;
134 | req.log = weblog;
135 | res.end = function(chunk, encoding) {
136 | var rec;
137 | res.end = end;
138 | res.end(chunk, encoding);
139 | rec = {req: req, res: res};
140 | weblog.info(rec);
141 | };
142 | next();
143 | };
144 | };
145 |
146 |
147 | if (_.has(config, "key")) {
148 |
149 | log.info("Using SSL");
150 |
151 | app = express.createServer({key: fs.readFileSync(config.key),
152 | cert: fs.readFileSync(config.cert)});
153 | bounce = express.createServer(function(req, res, next) {
154 | var host = req.header('Host');
155 | res.redirect('https://'+host+req.url, 301);
156 | });
157 |
158 | } else {
159 |
160 | log.info("Not using SSL");
161 |
162 | app = express.createServer();
163 | }
164 |
165 | // Configuration
166 |
167 | var dbstore = new DatabankStore(db, log, 60000);
168 |
169 | log.info("Configuring app");
170 |
171 | app.configure(function(){
172 | app.set('views', __dirname + '/views');
173 | app.set('view engine', 'utml');
174 | app.use(requestLogger(log));
175 | app.use(express.bodyParser());
176 | app.use(express.cookieParser());
177 | app.use(express.methodOverride());
178 | app.use(express.session({secret: (_(config).has('sessionSecret')) ? config.sessionSecret : "insecure",
179 | store: dbstore}));
180 | app.use(app.router);
181 | app.use(express.static(__dirname + '/public'));
182 | });
183 |
184 | app.configure('development', function(){
185 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
186 | });
187 |
188 | app.configure('production', function(){
189 | app.use(express.errorHandler());
190 | });
191 |
192 | // Auth middleware
193 |
194 | var userAuth = function(req, res, next) {
195 |
196 | req.user = null;
197 | res.local("user", null);
198 |
199 | if (!req.session.farmerID) {
200 | next();
201 | } else {
202 | Farmer.get(req.session.farmerID, function(err, farmer) {
203 | if (err) {
204 | next(err);
205 | } else {
206 | req.user = farmer;
207 | res.local("user", farmer);
208 | next();
209 | }
210 | });
211 | }
212 | };
213 |
214 | var userOptional = function(req, res, next) {
215 | next();
216 | };
217 |
218 | var userRequired = function(req, res, next) {
219 | if (!req.user) {
220 | next(new Error("User is required"));
221 | } else {
222 | next();
223 | }
224 | };
225 |
226 | var noUser = function(req, res, next) {
227 | if (req.user) {
228 | next(new Error("Already logged in"));
229 | } else {
230 | next();
231 | }
232 | };
233 |
234 | var userIsFarmer = function(req, res, next) {
235 | if (req.params.webfinger && req.user.id == req.params.webfinger) {
236 | next();
237 | } else {
238 | next(new Error("Must be the same farmer"));
239 | }
240 | };
241 |
242 | var reqPlot = function(req, res, next) {
243 |
244 | var uuid = req.params.plot;
245 |
246 | Plot.get(uuid, function(err, plot) {
247 | if (err) {
248 | next(err);
249 | } else {
250 | req.plot = plot;
251 | next();
252 | }
253 | });
254 | };
255 |
256 | var reqCrop = function(req, res, next) {
257 |
258 | var uuid = req.params.crop;
259 |
260 | Crop.get(uuid, function(err, crop) {
261 | if (err) {
262 | next(err);
263 | } else {
264 | req.crop = crop;
265 | next();
266 | }
267 | });
268 | };
269 |
270 | var userIsOwner = function(req, res, next) {
271 | if (req.user.id == req.plot.owner) {
272 | next();
273 | } else {
274 | next(new Error("Must be the owner"));
275 | }
276 | };
277 |
278 | // Routes
279 |
280 | log.info("Initializing routes");
281 |
282 | app.get('/', userAuth, userOptional, routes.index);
283 | app.get('/login', userAuth, noUser, routes.login);
284 | app.post('/login', userAuth, noUser, routes.handleLogin);
285 | app.post('/logout', userAuth, userRequired, routes.handleLogout);
286 | app.get('/about', userAuth, userOptional, routes.about);
287 | app.get('/authorized/:hostname', routes.authorized);
288 | app.get('/farmer/:webfinger', userAuth, userOptional, routes.farmer);
289 | app.get('/plot/:plot', userAuth, userOptional, reqPlot, routes.plot);
290 | app.get('/crop/:crop', userAuth, userOptional, reqCrop, routes.crop);
291 | app.get('/plot/:plot/plant', userAuth, userRequired, reqPlot, userIsOwner, routes.plant);
292 | app.post('/plot/:plot/plant', userAuth, userRequired, reqPlot, userIsOwner, routes.handlePlant);
293 | app.get('/plot/:plot/tearup', userAuth, userRequired, reqPlot, userIsOwner, routes.tearUp);
294 | app.post('/plot/:plot/tearup', userAuth, userRequired, reqPlot, userIsOwner, routes.handleTearUp);
295 | app.get('/plot/:plot/water', userAuth, userRequired, reqPlot, userIsOwner, routes.water);
296 | app.post('/plot/:plot/water', userAuth, userRequired, reqPlot, userIsOwner, routes.handleWater);
297 | app.get('/plot/:plot/harvest', userAuth, userRequired, reqPlot, userIsOwner, routes.harvest);
298 | app.post('/plot/:plot/harvest', userAuth, userRequired, reqPlot, userIsOwner, routes.handleHarvest);
299 | app.get('/buy-plot', userAuth, userRequired, routes.buyPlot);
300 | app.post('/buy-plot', userAuth, userRequired, routes.handleBuyPlot);
301 | app.get('/.well-known/host-meta.json', routes.hostmeta);
302 |
303 | // Create a dialback client
304 |
305 | log.info("Initializing dialback client");
306 |
307 | client = new DialbackClient({
308 | hostname: config.hostname,
309 | app: app,
310 | bank: db,
311 | userAgent: "OpenFarmGame/0.1.0"
312 | });
313 |
314 | // Configure this global object
315 |
316 | Host.dialbackClient = client;
317 |
318 | // Configure the service object
319 |
320 | log.info({name: config.name,
321 | description: config.description,
322 | hostname: config.hostname},
323 | "Initializing OpenFarmGame object");
324 |
325 | OpenFarmGame.name = config.name;
326 | OpenFarmGame.description = config.description;
327 | OpenFarmGame.hostname = config.hostname;
328 |
329 | OpenFarmGame.protocol = (config.key) ? "https" : "http";
330 |
331 | // Let Web stuff get to config
332 |
333 | app.config = config;
334 |
335 | // For sending notifications
336 |
337 | log.info("Initializing notifier");
338 |
339 | var notifier = new Notifier();
340 |
341 | app.notify = function(farmer, title, template, data, callback) {
342 | notifier.notify(farmer, title, template, data, callback);
343 | };
344 |
345 | // For handling errors
346 |
347 | app.log = function(obj) {
348 | if (obj instanceof Error) {
349 | log.error(obj);
350 | } else {
351 | log.info(obj);
352 | }
353 | };
354 |
355 | // updater -- keeps the world up-to-date
356 | // XXX: move to master process when clustering
357 |
358 | log.info("Initializing updater");
359 |
360 | app.updater = new Updater({notifier: notifier, log: log});
361 |
362 | app.updater.start();
363 |
364 | // Start the app
365 |
366 | log.info({port: config.port, address: config.address}, "Starting app listener");
367 |
368 | app.listen(config.port, config.address, callback);
369 |
370 | // Start the bouncer
371 |
372 | if (bounce) {
373 | log.info({port: 80, address: config.address}, "Starting bounce listener");
374 | bounce.listen(80, config.address);
375 | }
376 |
377 | }], function(err) {
378 | if (err) {
379 | log.error(err);
380 | } else {
381 | console.log("Express server listening on address %s port %d", config.address, config.port);
382 | }
383 | });
384 |
--------------------------------------------------------------------------------
/lib/updater.js:
--------------------------------------------------------------------------------
1 | // updater.js
2 | //
3 | // Updates the state of the world and notifies farmers of it
4 | //
5 | // Copyright 2013, StatusNet Inc.
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | var _ = require("underscore"),
20 | async = require("async"),
21 | Farmer = require("../models/farmer"),
22 | Host = require("../models/host"),
23 | Plot = require("../models/plot"),
24 | Crop = require("../models/crop"),
25 | CropType = require("../models/croptype"),
26 | OpenFarmGame = require("../models/openfarmgame");
27 |
28 | var ignore = function(err) {};
29 |
30 | var S = 1000;
31 | var M = 60 * S;
32 | var H = 60 * M;
33 |
34 | var Updater = function(options) {
35 |
36 | var notifier = options.notifier,
37 | log = options.log.child({component: "updater"}),
38 | logError = function(err) {
39 | log.error(err);
40 | },
41 | updateFarmer = function(id, callback) {
42 | log.info({id: id}, "Updating farmer");
43 | async.waterfall([
44 | function(callback) {
45 | Farmer.get(id, callback);
46 | },
47 | function(farmer, callback) {
48 | log.info({farmer: farmer}, "Got farmer");
49 | _.each(farmer.plots, function(uuid) {
50 | log.info({farmer: farmer.id, plot: uuid}, "Queueing plot");
51 | plotQueue.push(uuid, function(err) {
52 | if (err) {
53 | log.error(err);
54 | } else {
55 | log.info({plot: uuid}, "Finished updating plot");
56 | }
57 | });
58 | });
59 | callback(null);
60 | }
61 | ], callback);
62 | },
63 | notifyEmptyPlot = function(farmer, plot, callback) {
64 | notifier.notify(farmer, "Your plot is empty", "emptyplot", {plot: plot}, callback);
65 | },
66 | updateEmptyPlot = function(plot, callback) {
67 | var now = Date.now();
68 | log.info("Checking empty plot");
69 | if (now - plot.updated > Updater.EMPTY_NOTIFICATION_TIME &&
70 | !plot.emptyNotified) {
71 | log.info("Notifying farmer of empty plot of land");
72 | async.waterfall([
73 | function(callback) {
74 | Farmer.get(plot.owner, callback);
75 | },
76 | function(farmer, callback) {
77 | notifyEmptyPlot(farmer, plot, callback);
78 | },
79 | function(callback) {
80 | plot.emptyNotified = now;
81 | plot.save(callback);
82 | }
83 | ], callback);
84 | } else {
85 | callback(null);
86 | }
87 | return;
88 | },
89 | updateCrop = function(uuid, callback) {
90 |
91 | var crop,
92 | type,
93 | farmer,
94 | now = Date.now();
95 |
96 | async.waterfall([
97 | function(callback) {
98 | log.info({crop: uuid}, "Getting crop");
99 | Crop.get(uuid, callback);
100 | },
101 | function(results, callback) {
102 | crop = results;
103 | log.info({type: crop.type}, "Getting crop type");
104 | CropType.get(crop.type, callback);
105 | },
106 | function(results, callback) {
107 | type = results;
108 | log.info({farmer: crop.owner}, "Getting crop owner");
109 | Farmer.get(crop.owner, callback);
110 | },
111 | function(results, callback) {
112 | farmer = results;
113 | log.info({type: type, crop: crop}, "Checking crop");
114 | switch (crop.state) {
115 | case Crop.GROWING:
116 | if (crop.watered >= type.waterings) {
117 | if ((now - crop.updated) >= type.ripentime * M) {
118 | log.info("Setting crop state to ripe");
119 | async.waterfall([
120 | function(callback) {
121 | crop.state = Crop.RIPE;
122 | crop.save(callback);
123 | },
124 | function(crop, callback) {
125 | notifier.notify(farmer,
126 | "Your " + crop.name + " is ready to harvest",
127 | "ripecrop",
128 | {crop: crop},
129 | callback);
130 | }
131 | ], callback);
132 | } else {
133 | log.info("No change");
134 | callback(null, crop);
135 | }
136 | } else if (now - crop.updated > type.watertime * M) {
137 | log.info("Setting crop state to needs water");
138 | async.waterfall([
139 | function(callback) {
140 | crop.state = Crop.NEEDS_WATER;
141 | crop.save(callback);
142 | },
143 | function(crop, callback) {
144 | notifier.notify(farmer,
145 | "Your " + crop.name + " needs water",
146 | "needswater",
147 | {crop: crop},
148 | callback);
149 | }
150 | ], callback);
151 | } else {
152 | log.info("No change");
153 | callback(null, crop);
154 | }
155 | break;
156 | case Crop.NEW: // new stuff needs water
157 | case Crop.NEEDS_WATER:
158 | if (now - crop.updated > type.reallywatertime * M) {
159 | log.info("Setting crop state to really needs water");
160 | async.waterfall([
161 | function(callback) {
162 | crop.state = Crop.REALLY_NEEDS_WATER;
163 | crop.damaged = true;
164 | crop.save(callback);
165 | },
166 | function(crop, callback) {
167 | notifier.notify(farmer,
168 | "Your " + crop.name + " is parched",
169 | "reallyneedswater",
170 | {crop: crop},
171 | callback);
172 | }
173 | ], callback);
174 | } else {
175 | log.info("No change");
176 | callback(null, crop);
177 | }
178 | break;
179 | case Crop.REALLY_NEEDS_WATER:
180 | if (now - crop.updated > type.dehydrationtime * M) {
181 | log.info("Setting crop state to dead");
182 | async.waterfall([
183 | function(callback) {
184 | crop.state = Crop.DEAD;
185 | crop.save(callback);
186 | },
187 | function(crop, callback) {
188 | notifier.notify(farmer,
189 | "Your " + crop.name + " is dead from dehydration",
190 | "dehydration",
191 | {crop: crop},
192 | callback);
193 | }
194 | ], callback);
195 | } else {
196 | log.info("No change");
197 | callback(null, crop);
198 | }
199 | break;
200 | case Crop.RIPE:
201 | if (now - crop.updated > type.overripentime * M) {
202 | log.info("Setting crop state to overripe");
203 | async.waterfall([
204 | function(callback) {
205 | crop.state = Crop.OVERRIPE;
206 | crop.damaged = true;
207 | crop.save(callback);
208 | },
209 | function(crop, callback) {
210 | notifier.notify(farmer,
211 | "Your " + crop.name + " is overripe",
212 | "overripe",
213 | {crop: crop},
214 | callback);
215 | }
216 | ], callback);
217 | } else {
218 | log.info("No change");
219 | callback(null, crop);
220 | }
221 | break;
222 | case Crop.OVERRIPE:
223 | if (now - crop.updated > type.rottime * M) {
224 | log.info("Setting crop state to dead");
225 | async.waterfall([
226 | function(callback) {
227 | crop.state = Crop.DEAD;
228 | crop.save(callback);
229 | },
230 | function(crop, callback) {
231 | notifier.notify(farmer,
232 | "Your " + crop.name + " rotted in the fields",
233 | "rotten",
234 | {crop: crop},
235 | callback);
236 | }
237 | ], callback);
238 | } else {
239 | log.info("No change");
240 | callback(null, crop);
241 | }
242 | break;
243 | default:
244 | log.info({state: crop.state}, "Unrecognized state");
245 | callback(null, crop);
246 | break;
247 | }
248 | }
249 | ], callback);
250 | },
251 | updatePlot = function(uuid, callback) {
252 |
253 | var plot;
254 |
255 | log.info({plot: uuid}, "Updating plot");
256 |
257 | async.waterfall([
258 | function(callback) {
259 | Plot.get(uuid, callback);
260 | },
261 | function(results, callback) {
262 | plot = results;
263 | if (!plot.crop) {
264 | log.info({plot: uuid}, "Updating empty plot");
265 | updateEmptyPlot(plot, callback);
266 | } else {
267 | log.info({plot: uuid, crop: plot.crop}, "Updating crop");
268 | updateCrop(plot.crop, callback);
269 | }
270 | }
271 | ], callback);
272 | },
273 | updateAll = function() {
274 | var bank = Farmer.bank();
275 | bank.read("farmerlist", 0, function(err, list) {
276 | if (err) {
277 | log.info(err, "Error getting farmerlist.");
278 | } else if (list.length === 0) {
279 | log.info("No farmers.");
280 | } else {
281 | log.info(list, "Got farmerlist");
282 | _.each(list, function(farmer) {
283 | farmerQueue.push(farmer, function(err) {
284 | if (err) {
285 | log.info(err, err.message);
286 | } else {
287 | log.info({farmer: farmer}, "Finished updating");
288 | }
289 | });
290 | });
291 | }
292 | });
293 | },
294 | queueStats = function() {
295 | log.info({farmers: farmerQueue.length(), plots: plotQueue.length()}, "Queue stats");
296 | },
297 | farmerQueue = async.queue(updateFarmer, 25),
298 | plotQueue = async.queue(updatePlot, 25);
299 |
300 | farmerQueue.drain = function() {
301 | log.info("Farmer queue empty;.");
302 | };
303 |
304 | plotQueue.drain = function() {
305 | log.info("Plot queue empty.");
306 | };
307 |
308 | this.notifier = options.notifier;
309 |
310 | this.start = function() {
311 | // Do this every 15 minutes
312 | setInterval(updateAll, 15 * M);
313 | // Do this every minute
314 | setInterval(queueStats, 1 * M);
315 | // Do one right now
316 | updateAll();
317 | };
318 | };
319 |
320 | Updater.EMPTY_NOTIFICATION_TIME = 24 * H;
321 |
322 | module.exports = Updater;
323 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | // index.js
2 | //
3 | // Most of the routes in the application
4 | //
5 | // Copyright 2013, StatusNet Inc.
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | var wf = require("webfinger"),
20 | async = require("async"),
21 | _ = require("underscore"),
22 | uuid = require("node-uuid"),
23 | Farmer = require("../models/farmer"),
24 | Plot = require("../models/plot"),
25 | Crop = require("../models/crop"),
26 | CropType = require("../models/croptype"),
27 | Host = require("../models/host"),
28 | RequestToken = require("../models/requesttoken"),
29 | OpenFarmGame = require("../models/openfarmgame");
30 |
31 | exports.hostmeta = function(req, res) {
32 | res.json({
33 | links: [
34 | {
35 | rel: "dialback",
36 | href: OpenFarmGame.url("/dialback")
37 | }
38 | ]
39 | });
40 | };
41 |
42 | exports.index = function(req, res, next) {
43 | var plots;
44 |
45 | if (req.user) {
46 | async.waterfall([
47 | function(callback) {
48 | Plot.readArray(req.user.plots, callback);
49 | },
50 | function(results, callback) {
51 | var cropIDs;
52 | plots = results;
53 | cropIDs = _.compact(_.pluck(plots, "crop"));
54 | if (cropIDs.length > 0) {
55 | Crop.readAll(cropIDs, callback);
56 | } else {
57 | callback(null, []);
58 | }
59 | }
60 | ], function(err, crops) {
61 | if (err) {
62 | next(err);
63 | } else {
64 | res.render("farmer", {title: "Open Farm Game", user: req.user, farmer: req.user, plots: plots, crops: crops});
65 | }
66 | });
67 | } else {
68 | res.render('index', { title: "Open Farm Game" });
69 | }
70 | };
71 |
72 | exports.about = function(req, res) {
73 | res.render('about', { title: 'About Open Farm Game' });
74 | };
75 |
76 | exports.login = function(req, res) {
77 | res.render('login', { title: 'Login' });
78 | };
79 |
80 | exports.handleLogin = function(req, res, next) {
81 |
82 | var id = req.body.webfinger,
83 | hostname = Farmer.getHostname(id),
84 | host;
85 |
86 | async.waterfall([
87 | function(callback) {
88 | Host.ensureHost(hostname, callback);
89 | },
90 | function(results, callback) {
91 | host = results;
92 | host.getRequestToken(callback);
93 | }
94 | ], function(err, rt) {
95 | if (err) {
96 | if (err instanceof Error) {
97 | next(err);
98 | } else if (err.data) {
99 | next(new Error(err.data));
100 | }
101 | } else {
102 | res.redirect(host.authorizeURL(rt));
103 | }
104 | });
105 | };
106 |
107 | exports.authorized = function(req, res, next) {
108 |
109 | var hostname = req.params.hostname,
110 | token = req.query.oauth_token,
111 | verifier = req.query.oauth_verifier,
112 | rt,
113 | host,
114 | access_token,
115 | token_secret,
116 | id,
117 | object,
118 | newFarmer = false;
119 |
120 | async.waterfall([
121 | function(callback) {
122 | async.parallel([
123 | function(callback) {
124 | RequestToken.get(RequestToken.key(hostname, token), callback);
125 | },
126 | function(callback) {
127 | Host.get(hostname, callback);
128 | }
129 | ], callback);
130 | },
131 | function(results, callback) {
132 | rt = results[0];
133 | host = results[1];
134 | host.getAccessToken(rt, verifier, callback);
135 | },
136 | function(token, secret, extra, callback) {
137 | access_token = token;
138 | token_secret = secret;
139 | async.parallel([
140 | function(callback) {
141 | rt.del(callback);
142 | },
143 | function(callback) {
144 | host.whoami(access_token, token_secret, callback);
145 | }
146 | ], callback);
147 | },
148 | function(results, callback) {
149 | object = results[1];
150 | id = object.id;
151 | if (id.substr(0, 5) == "acct:") {
152 | id = id.substr(5);
153 | }
154 | Farmer.get(id, function(err, farmer) {
155 | if (err && err.name === "NoSuchThingError") {
156 | newFarmer = true;
157 | Farmer.fromPerson(object, access_token, token_secret, callback);
158 | } else if (err) {
159 | callback(err, null);
160 | } else {
161 | callback(null, farmer);
162 | }
163 | });
164 | }
165 | ], function(err, farmer) {
166 | if (err) {
167 | next(err);
168 | } else {
169 | req.session.farmerID = farmer.id;
170 | res.redirect("/");
171 | if (newFarmer) {
172 | process.nextTick(function() {
173 | async.parallel([
174 | function(callback) {
175 | farmer.joinActivity(callback);
176 | },
177 | function(callback) {
178 | req.app.notify(farmer,
179 | "Welcome to " + OpenFarmGame.name,
180 | "welcome",
181 | {farmer: farmer},
182 | callback);
183 | }
184 | ], function(err, results) {
185 | if (err) {
186 | req.app.log(err);
187 | }
188 | });
189 | });
190 | }
191 | }
192 | });
193 | };
194 |
195 | exports.handleLogout = function(req, res) {
196 |
197 | delete req.session.farmerID;
198 | delete req.user;
199 |
200 | res.redirect("/", 303);
201 | };
202 |
203 | exports.farmer = function(req, res, next) {
204 |
205 | var id = req.params.webfinger,
206 | farmer,
207 | plots;
208 |
209 | async.waterfall([
210 | function(callback) {
211 | Farmer.get(id, callback);
212 | },
213 | function(results, callback) {
214 | farmer = results;
215 | Plot.readArray(farmer.plots, callback);
216 | },
217 | function(results, callback) {
218 | var cropIDs;
219 | plots = results;
220 | cropIDs = _.compact(_.pluck(plots, "crop"));
221 | if (cropIDs.length > 0) {
222 | Crop.readAll(cropIDs, callback);
223 | } else {
224 | callback(null, []);
225 | }
226 | }
227 | ], function(err, crops) {
228 | if (err) {
229 | next(err);
230 | } else {
231 | res.render("farmer", {title: "Farmer " + farmer.name,
232 | user: req.user,
233 | farmer: farmer,
234 | plots: plots,
235 | crops: crops});
236 | }
237 | });
238 | };
239 |
240 | exports.tearUp = function(req, res, next) {
241 |
242 | var plot = req.plot;
243 |
244 | Crop.get(plot.crop, function(err, crop) {
245 | if (err) {
246 | next(err);
247 | } else {
248 | res.render('tearup', { title: 'Tear up a crop',
249 | farmer: req.user,
250 | plot: plot,
251 | crop: crop });
252 | }
253 | });
254 | };
255 |
256 | exports.handleTearUp = function(req, res, next) {
257 |
258 | var plot = req.plot,
259 | crop;
260 |
261 | async.waterfall([
262 | function(callback) {
263 | Crop.get(plot.crop, callback);
264 | },
265 | function(results, callback) {
266 | crop = results;
267 | plot.crop = null;
268 | plot.emptyNotified = null;
269 | plot.save(callback);
270 | },
271 | function(saved, callback) {
272 | crop.del(callback);
273 | }
274 | ], function(err) {
275 | if (err) {
276 | next(err);
277 | } else {
278 | res.redirect("/");
279 | req.user.tearUpActivity(crop, function(err) {});
280 | }
281 | });
282 | };
283 |
284 | exports.water = function(req, res, next) {
285 |
286 | var plot = req.plot;
287 |
288 | Crop.get(plot.crop, function(err, crop) {
289 | if (err) {
290 | next(err);
291 | } else {
292 | res.render('water', {title: 'Water a crop', farmer: req.user, plot: plot, crop: crop});
293 | }
294 | });
295 | };
296 |
297 | exports.handleWater = function(req, res, next) {
298 |
299 | var plot = req.plot;
300 |
301 | if (req.user.coins < 1) {
302 | next(new Error("Not enough coins to water something."));
303 | return;
304 | }
305 |
306 | async.waterfall([
307 | function(callback) {
308 | req.user.coins -= 1;
309 | req.user.save(callback);
310 | },
311 | function(saved, callback) {
312 | Crop.get(plot.crop, callback);
313 | },
314 | function(crop, callback) {
315 |
316 | crop.watered++;
317 | crop.state = Crop.GROWING;
318 |
319 | crop.save(callback);
320 | }
321 | ], function(err, crop) {
322 | if (err) {
323 | next(err);
324 | } else {
325 | res.redirect("/");
326 | req.user.waterActivity(crop, function(err) {});
327 | }
328 | });
329 | };
330 |
331 | exports.plant = function(req, res, next) {
332 |
333 | var plot = req.plot;
334 |
335 | CropType.getAll(function(err, types) {
336 | res.render('plant', { title: 'Plant a new crop', farmer: req.user, plot: plot, types: types });
337 | });
338 | };
339 |
340 | exports.handlePlant = function(req, res, next) {
341 |
342 | var plot = req.plot,
343 | slug = req.body.type,
344 | type,
345 | crop,
346 | now = Date.now();
347 |
348 | async.waterfall([
349 | function(callback) {
350 | CropType.get(slug, callback);
351 | },
352 | function(results, callback) {
353 |
354 | type = results;
355 |
356 | if (type.cost > req.user.coins) {
357 | callback(new Error("Not enough coins"), null);
358 | return;
359 | }
360 |
361 | req.user.coins -= type.cost;
362 |
363 | req.user.save(callback);
364 | },
365 | function(saved, callback) {
366 | Crop.create({type: type.slug,
367 | plot: plot.uuid,
368 | owner: req.user.id,
369 | name: type.name,
370 | state: Crop.NEW},
371 | callback);
372 | },
373 | function(results, callback) {
374 | crop = results;
375 | plot.crop = crop.uuid;
376 | plot.emptyNotified = null;
377 | plot.save(callback);
378 | }
379 | ], function(err) {
380 | if (err) {
381 | next(err);
382 | } else {
383 | res.redirect("/");
384 | req.user.plantActivity(crop, function(err) {});
385 | }
386 | });
387 | };
388 |
389 | exports.buyPlot = function(req, res, next) {
390 |
391 | res.render('buy-plot', { title: 'Buy a plot', farmer: req.user });
392 | };
393 |
394 | exports.handleBuyPlot = function(req, res, next) {
395 |
396 | var plot;
397 |
398 | if (req.user.coins < 50) {
399 | next(new Error("Not enough coins to buy a plot."));
400 | return;
401 | }
402 |
403 | async.waterfall([
404 | function(callback) {
405 | Plot.create({owner: req.user.id}, callback);
406 | },
407 | function(results, callback) {
408 | plot = results;
409 | req.user.coins -= 50;
410 | req.user.plots.push(plot.uuid);
411 | req.user.save(callback);
412 | }
413 | ], function(err, saved) {
414 | if (err) {
415 | next(err);
416 | } else {
417 | res.redirect("/");
418 | req.user.buyActivity(plot, function(err) {});
419 | }
420 | });
421 | };
422 |
423 | exports.harvest = function(req, res, next) {
424 |
425 | var plot = req.plot,
426 | crop;
427 |
428 | async.waterfall([
429 | function(callback) {
430 | Crop.get(plot.crop, callback);
431 | },
432 | function(results, callback) {
433 | crop = results;
434 | CropType.get(crop.type, callback);
435 | }
436 | ], function(err, type) {
437 | if (err) {
438 | next(err);
439 | } else {
440 | res.render('harvest', { title: 'Harvest a crop', farmer: req.user, plot: plot, crop: crop, type: type });
441 | }
442 | });
443 | };
444 |
445 | exports.handleHarvest = function(req, res, next) {
446 |
447 | var plot = req.plot,
448 | crop,
449 | type;
450 |
451 | async.waterfall([
452 | function(callback) {
453 | Crop.get(plot.crop, callback);
454 | },
455 | function(results, callback) {
456 | crop = results;
457 | CropType.get(crop.type, callback);
458 | },
459 | function(results, callback) {
460 | type = results;
461 | async.parallel([
462 | function(callback) {
463 | crop.state = Crop.HARVESTED;
464 | crop.save(callback);
465 | },
466 | function(callback) {
467 | plot.crop = null;
468 | plot.emptyNotified = null;
469 | plot.save(callback);
470 | },
471 | function(callback) {
472 | // Dogfood or something
473 | if (crop.damaged) {
474 | req.user.coins += Math.ceil(type.price/2.0);
475 | } else {
476 | req.user.coins += type.price;
477 | }
478 | req.user.save(callback);
479 | }
480 | ], callback);
481 | }
482 | ], function(err, results) {
483 | if (err) {
484 | next(err);
485 | } else {
486 | res.redirect("/");
487 | req.user.harvestActivity(crop, function(err) {});
488 | }
489 | });
490 | };
491 |
492 | exports.plot = function(req, res, next) {
493 |
494 | var plot = req.plot;
495 |
496 | async.parallel([
497 | function(callback) {
498 | if (plot.crop) {
499 | Crop.get(plot.crop, callback);
500 | } else {
501 | callback(null, null);
502 | }
503 | },
504 | function(callback) {
505 | Farmer.get(plot.owner, callback);
506 | }
507 | ], function(err, results) {
508 | var crop, farmer;
509 |
510 | if (err) {
511 | next(err);
512 | } else {
513 | crop = results[0];
514 | farmer = results[1];
515 | res.render('plotpage', {title: 'A plot by ' + farmer.name,
516 | user: req.user,
517 | farmer: farmer,
518 | plot: plot,
519 | crop: crop});
520 | }
521 | });
522 | };
523 |
524 | exports.crop = function(req, res, next) {
525 |
526 | var crop = req.crop;
527 |
528 | async.parallel([
529 | function(callback) {
530 | Plot.get(crop.plot, callback);
531 | },
532 | function(callback) {
533 | Farmer.get(crop.owner, callback);
534 | }
535 | ], function(err, results) {
536 | var plot, farmer;
537 |
538 | if (err) {
539 | next(err);
540 | } else {
541 | plot = results[0];
542 | farmer = results[1];
543 | res.render('croppage', {title: crop.name + ' by ' + farmer.name,
544 | user: req.user,
545 | farmer: farmer,
546 | plot: plot,
547 | crop: crop});
548 | }
549 | });
550 | };
551 |
--------------------------------------------------------------------------------