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 }