1 module vibelog.db.mongo;
2 
3 import vibelog.db.dbcontroller;
4 
5 import vibe.core.log;
6 import vibe.core.stream;
7 import vibe.crypto.passwordhash;
8 import vibe.data.bson;
9 import vibe.db.mongo.mongo;
10 import vibe.mail.smtp;
11 import vibe.stream.memory;
12 import vibe.templ.diet;
13 
14 import std.exception;
15 import std.variant;
16 
17 
18 final class MongoDBController : DBController {
19 	private {
20 		MongoCollection m_configs;
21 		MongoCollection m_users;
22 		MongoCollection m_posts;
23 		MongoCollection m_postFiles;
24 		MongoCollection m_comments;
25 		void delegate()[] m_onConfigChange;
26 	}
27 
28 	this(string db_url)
29 	{
30 		string database = "vibelog";
31 		MongoClientSettings dbsettings;
32 		if (parseMongoDBUrl(dbsettings, db_url))
33 			database = dbsettings.database;
34 
35 		auto db = connectMongoDB(db_url).getDatabase(database);
36 		m_configs = db["configs"];
37 		m_users = db["users"];
38 		m_posts = db["posts"];
39 		m_postFiles = db["postFiles"];
40 		m_comments = db["comments"];
41 
42 		// Upgrade post contained comments to their collection
43 		foreach( p; m_posts.find(["comments": ["$exists": true]], ["comments": 1]) ){
44 			foreach( c; p.comments ){
45 				c["_id"] = BsonObjectID.generate();
46 				c["postId"] = p._id;
47 				m_comments.insert(c);
48 			}
49 			m_posts.update(["_id": p._id], ["$unset": ["comments": 1]]);
50 		}
51 	}
52 
53 	Config getConfig(string name, bool createdefault = false)
54 	{
55 		auto configbson = m_configs.findOne(["name": Bson(name)]);
56 		if( !configbson.isNull() )
57 			return Config.fromBson(configbson);
58 		enforce(createdefault, "Configuration does not exist.");
59 		auto cfg = new Config;
60 		cfg.name = name;
61 		m_configs.insert(cfg.toBson());
62 		return cfg;
63 	}
64 
65 	void setConfig(Config cfg)
66 	{
67 		Bson update = cfg.toBson();
68 		m_configs.update(["name": Bson(cfg.name)], update);
69 		foreach (d; m_onConfigChange) d();
70 	}
71 
72 	void invokeOnConfigChange(void delegate() del)
73 	{
74 		m_onConfigChange ~= del;
75 	}
76 
77 	void deleteConfig(string name)
78 	{
79 		m_configs.remove(["name": Bson(name)]);
80 	}
81 
82 	Config[] getAllConfigs()
83 	{
84 		Bson[string] query;
85 		Config[] ret;
86 		foreach( config; m_configs.find(query) ){
87 			auto c = Config.fromBson(config);
88 			ret ~= c;
89 		}
90 		return ret;
91 	}
92 
93 	User[string] getAllUsers()
94 	{
95 		Bson[string] query;
96 		User[string] ret;
97 		foreach( user; m_users.find(query) ){
98 			auto u = User.fromBson(user);
99 			ret[u.username] = u;
100 		}
101 		if( ret.length == 0 ){
102 			auto initial_admin = new User;
103 			initial_admin.username = "admin";
104 			initial_admin.password = generateSimplePasswordHash("admin");
105 			initial_admin.name = "Default Administrator";
106 			initial_admin.groups ~= "admin";
107 			m_users.insert(initial_admin);
108 			ret["admin"] = initial_admin;
109 		}
110 		return ret;
111 	}
112 	
113 	User getUser(BsonObjectID userid)
114 	{
115 		auto userbson = m_users.findOne(["_id": Bson(userid)]);
116 		return User.fromBson(userbson);
117 	}
118 
119 	User getUser(string name)
120 	{
121 		auto userbson = m_users.findOne(["username": Bson(name)]);
122 		if( userbson.isNull() ){
123 			auto id = BsonObjectID.fromHexString(name);
124 			logDebug("%s <-> %s", name, id.toString());
125 			assert(id.toString() == name);
126 			userbson = m_users.findOne(["_id": Bson(id)]);
127 		}
128 		//auto userbson = m_users.findOne(Bson(["name" : Bson(name)]));
129 		return User.fromBson(userbson);
130 	}
131 
132 	BsonObjectID addUser(User user)
133 	{
134 		auto id = BsonObjectID.generate();
135 		Bson userbson = user.toBson();
136 		userbson["_id"] = Bson(id);
137 		m_users.insert(userbson);
138 		return id;
139 	}
140 
141 	void modifyUser(User user)
142 	{
143 		assert(user._id.valid);
144 		Bson update = user.toBson();
145 		m_users.update(["_id": Bson(user._id)], update);
146 	}
147 
148 	void deleteUser(BsonObjectID id)
149 	{
150 		assert(id.valid);
151 		m_users.remove(["_id": Bson(id)]);
152 	}
153 
154 	int countPostsForCategory(string[] categories)
155 	{
156 		int cnt;
157 		getPostsForCategory(categories, 0, (size_t, Post p){ if( p.isPublic ) cnt++; return true; });
158 		return cnt;
159 	}
160 
161 	void getPostsForCategory(string[] categories, int nskip, bool delegate(size_t idx, Post post) del)
162 	{
163 		auto cats = new Bson[categories.length];
164 		foreach( i; 0 .. categories.length ) cats[i] = Bson(categories[i]);
165 		Bson category = Bson(["$in" : Bson(cats)]);
166 		Bson[string] query = ["query" : Bson(["category" : category]), "orderby" : Bson(["_id" : Bson(-1)])];
167 		foreach( idx, post; m_posts.find(query, null, QueryFlags.None, nskip) ){
168 			if( !del(idx, Post.fromBson(post)) )
169 				break;
170 		}
171 	}
172 
173 	void getPublicPostsForCategory(string[] categories, int nskip, bool delegate(size_t idx, Post post) del)
174 	{
175 		auto cats = new Bson[categories.length];
176 		foreach( i; 0 .. categories.length ) cats[i] = Bson(categories[i]);
177 		Bson category = Bson(["$in" : Bson(cats)]);
178 		Bson[string] query = ["query" : Bson(["category" : category, "isPublic": Bson(true)]), "orderby" : Bson(["_id" : Bson(-1)])];
179 		foreach( idx, post; m_posts.find(query, null, QueryFlags.None, nskip) ){
180 			if( !del(idx, Post.fromBson(post)) )
181 				break;
182 		}
183 	}
184 
185 	void getAllPosts(int nskip, bool delegate(size_t idx, Post post) del)
186 	{
187 		Bson[string] query;
188 		Bson[string] extquery = ["query" : Bson(query), "orderby" : Bson(["_id" : Bson(-1)])];
189 		foreach( idx, post; m_posts.find(extquery, null, QueryFlags.None, nskip) ){
190 			if( !del(idx, Post.fromBson(post)) )
191 				break;
192 		}
193 	}
194 
195 
196 	Post getPost(BsonObjectID postid)
197 	{
198 		auto postbson = m_posts.findOne(["_id": Bson(postid)]);
199 		return Post.fromBson(postbson);
200 	}
201 
202 	Post getPost(string name)
203 	{
204 		auto postbson = m_posts.findOne(["slug": Bson(name)]);
205 		if( postbson.isNull() )
206 			postbson = m_posts.findOne(["_id" : Bson(BsonObjectID.fromHexString(name))]);
207 		return Post.fromBson(postbson);
208 	}
209 
210 	bool hasPost(string name)
211 	{
212 		return !m_posts.findOne(["slug": Bson(name)]).isNull();
213 
214 	}
215 
216 	BsonObjectID addPost(Post post)
217 	{
218 		auto id = BsonObjectID.generate();
219 		Bson postbson = post.toBson();
220 		postbson["_id"] = Bson(id);
221 		m_posts.insert(postbson);
222 		return id;
223 	}
224 
225 	void modifyPost(Post post)
226 	{
227 		assert(post.id.valid);
228 		Bson update = post.toBson();
229 		m_posts.update(["_id": Bson(post.id)], update);
230 	}
231 
232 	void deletePost(BsonObjectID id)
233 	{
234 		assert(id.valid);
235 		m_posts.remove(["_id": Bson(id)]);
236 	}
237 
238 	void addFile(string post_name, string file_name, InputStream contents)
239 	{
240 		import vibe.stream.operations : readAll;
241 		struct I {
242 			string postName;
243 			string fileName;
244 		}
245 		m_postFiles.insert(PostFile(post_name, file_name, contents.readAll()));
246 	}
247 
248 	string[] getFiles(string post_name)
249 	{
250 		return m_postFiles.find(["postName": post_name], ["fileName": true]).map!(p => p.fileName.get!string).array;
251 	}
252 
253 	InputStream getFile(string post_name, string file_name)
254 	{
255 		auto f = m_postFiles.findOne!PostFile(["postName": post_name, "fileName": file_name]);
256 		if (f.isNull) return null;
257 		return new MemoryStream(f.contents);
258 	}
259 
260 	void removeFile(string post_name, string file_name)
261 	{
262 		m_postFiles.remove(["postName": post_name, "fileName": file_name]);
263 	}
264 
265 	Comment[] getComments(BsonObjectID post_id, bool allow_inactive = false)
266 	{
267 		Comment[] ret;
268 		foreach( c; m_comments.find(["postId": post_id]) )
269 			if( allow_inactive || c.isPublic.get!bool )
270 				ret ~= Comment.fromBson(c);
271 		return ret;
272 	}
273 
274 	long getCommentCount(BsonObjectID post_id)
275 	{
276 		return m_comments.count(["postId": Bson(post_id), "isPublic": Bson(true)]);
277 	}
278 
279 
280 	void addComment(BsonObjectID post_id, Comment comment)
281 	{
282 		Bson cmtbson = comment.toBson();
283 		comment.id = BsonObjectID.generate();
284 		comment.postId = post_id;
285 		m_comments.insert(comment.toBson());
286 
287 		try {
288 			auto p = m_posts.findOne(["_id": post_id]);
289 			auto u = m_users.findOne(["username": p.author]);
290 			auto msg = new MemoryOutputStream;
291 
292 			auto post = Post.fromBson(p);
293 
294 			msg.parseDietFile!("mail.new_comment.dt", comment, post);
295 
296 			auto mail = new Mail;
297 			mail.headers["From"] = comment.authorName ~ " <" ~ comment.authorMail ~ ">";
298 			mail.headers["To"] = u.email.get!string;
299 			mail.headers["Subject"] = "[VibeLog] New comment";
300 			mail.headers["Content-Type"] = "text/html";
301 			mail.bodyText = cast(string)msg.data();
302 
303 			auto settings = new SMTPClientSettings;
304 			//settings.host = m_settings.mailServer;
305 			sendMail(settings, mail);
306 		} catch(Exception e){
307 			logWarn("Failed to send comment mail: %s", e.msg);
308 		}
309 	}
310 
311 	void setCommentPublic(BsonObjectID comment_id, bool is_public)
312 	{
313 		m_comments.update(["_id": comment_id], ["$set": ["isPublic": is_public]]);
314 	}
315 
316 	void deleteNonPublicComments(BsonObjectID post_id)
317 	{
318 		m_posts.remove(["postId": Bson(post_id), "isPublic": Bson(false)]);
319 	}
320 }
321 
322 struct PostFile {
323 	string postName;
324 	string fileName;
325 	ubyte[] contents;
326 }