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 void delegate()[] m_onConfigChange; 25 } 26 27 this(string db_url) 28 { 29 string database = "vibelog"; 30 MongoClientSettings dbsettings; 31 if (parseMongoDBUrl(dbsettings, db_url)) 32 database = dbsettings.database; 33 34 auto db = connectMongoDB(db_url).getDatabase(database); 35 m_configs = db["configs"]; 36 m_users = db["users"]; 37 m_posts = db["posts"]; 38 m_postFiles = db["postFiles"]; 39 40 upgradeComments(db); 41 } 42 43 Config getConfig(string name, bool createdefault = false) 44 { 45 auto configbson = m_configs.findOne(["name": Bson(name)]); 46 if( !configbson.isNull() ) 47 return Config.fromBson(configbson); 48 enforce(createdefault, "Configuration does not exist."); 49 auto cfg = new Config; 50 cfg.name = name; 51 m_configs.insert(cfg.toBson()); 52 return cfg; 53 } 54 55 void setConfig(Config cfg) 56 { 57 Bson update = cfg.toBson(); 58 m_configs.update(["name": Bson(cfg.name)], update); 59 foreach (d; m_onConfigChange) d(); 60 } 61 62 void invokeOnConfigChange(void delegate() del) 63 { 64 m_onConfigChange ~= del; 65 } 66 67 void deleteConfig(string name) 68 { 69 m_configs.remove(["name": Bson(name)]); 70 } 71 72 Config[] getAllConfigs() 73 { 74 Bson[string] query; 75 Config[] ret; 76 foreach( config; m_configs.find(query) ){ 77 auto c = Config.fromBson(config); 78 ret ~= c; 79 } 80 return ret; 81 } 82 83 User[string] getAllUsers() 84 { 85 Bson[string] query; 86 User[string] ret; 87 foreach( user; m_users.find(query) ){ 88 auto u = User.fromBson(user); 89 ret[u.username] = u; 90 } 91 if( ret.length == 0 ){ 92 auto initial_admin = new User; 93 initial_admin.username = "admin"; 94 initial_admin.password = generateSimplePasswordHash("admin"); 95 initial_admin.name = "Default Administrator"; 96 initial_admin.groups ~= "admin"; 97 m_users.insert(initial_admin); 98 ret["admin"] = initial_admin; 99 } 100 return ret; 101 } 102 103 User getUser(BsonObjectID userid) 104 { 105 auto userbson = m_users.findOne(["_id": Bson(userid)]); 106 return User.fromBson(userbson); 107 } 108 109 User getUserByName(string name) 110 { 111 auto userbson = m_users.findOne(["username": Bson(name)]); 112 if( userbson.isNull() ){ 113 auto id = BsonObjectID.fromHexString(name); 114 logDebug("%s <-> %s", name, id.toString()); 115 assert(id.toString() == name); 116 userbson = m_users.findOne(["_id": Bson(id)]); 117 } 118 //auto userbson = m_users.findOne(Bson(["name" : Bson(name)])); 119 return User.fromBson(userbson); 120 } 121 122 User getUserByEmail(string email) 123 { 124 auto userbson = m_users.findOne(["email": Bson(email)]); 125 return User.fromBson(userbson); 126 } 127 128 BsonObjectID addUser(User user) 129 { 130 auto id = BsonObjectID.generate(); 131 Bson userbson = user.toBson(); 132 userbson["_id"] = Bson(id); 133 m_users.insert(userbson); 134 return id; 135 } 136 137 void modifyUser(User user) 138 { 139 assert(user._id.valid); 140 Bson update = user.toBson(); 141 m_users.update(["_id": Bson(user._id)], update); 142 } 143 144 void deleteUser(BsonObjectID id) 145 { 146 assert(id.valid); 147 m_users.remove(["_id": Bson(id)]); 148 } 149 150 int countPostsForCategory(string[] categories) 151 { 152 int cnt; 153 getPostsForCategory(categories, 0, (size_t, Post p){ if( p.isPublic ) cnt++; return true; }); 154 return cnt; 155 } 156 157 void getPostsForCategory(string[] categories, int nskip, bool delegate(size_t idx, Post post) del) 158 { 159 auto cats = new Bson[categories.length]; 160 foreach( i; 0 .. categories.length ) cats[i] = Bson(categories[i]); 161 Bson category = Bson(["$in" : Bson(cats)]); 162 Bson[string] query = ["query" : Bson(["category" : category]), "orderby" : Bson(["date" : Bson(-1)])]; 163 foreach( idx, post; m_posts.find(query, null, QueryFlags.None, nskip) ){ 164 if( !del(idx, Post.fromBson(post)) ) 165 break; 166 } 167 } 168 169 void getPublicPostsForCategory(string[] categories, int nskip, bool delegate(size_t idx, Post post) del) 170 { 171 auto cats = new Bson[categories.length]; 172 foreach( i; 0 .. categories.length ) cats[i] = Bson(categories[i]); 173 Bson category = Bson(["$in" : Bson(cats)]); 174 Bson[string] query = ["query" : Bson(["category" : category, "isPublic": Bson(true)]), "orderby" : Bson(["date" : Bson(-1)])]; 175 foreach( idx, post; m_posts.find(query, null, QueryFlags.None, nskip) ){ 176 if( !del(idx, Post.fromBson(post)) ) 177 break; 178 } 179 } 180 181 void getAllPosts(int nskip, bool delegate(size_t idx, Post post) del) 182 { 183 Bson[string] query; 184 Bson[string] extquery = ["query" : Bson(query), "orderby" : Bson(["date" : Bson(-1)])]; 185 foreach( idx, post; m_posts.find(extquery, null, QueryFlags.None, nskip) ){ 186 if( !del(idx, Post.fromBson(post)) ) 187 break; 188 } 189 } 190 191 192 Post getPost(BsonObjectID postid) 193 { 194 auto postbson = m_posts.findOne(["_id": Bson(postid)]); 195 return Post.fromBson(postbson); 196 } 197 198 Post getPost(string name) 199 { 200 auto postbson = m_posts.findOne(["slug": Bson(name)]); 201 if( postbson.isNull() ) 202 postbson = m_posts.findOne(["_id" : Bson(BsonObjectID.fromHexString(name))]); 203 return Post.fromBson(postbson); 204 } 205 206 bool hasPost(string name) 207 { 208 return !m_posts.findOne(["slug": Bson(name)]).isNull(); 209 210 } 211 212 BsonObjectID addPost(Post post) 213 { 214 auto id = BsonObjectID.generate(); 215 Bson postbson = post.toBson(); 216 postbson["_id"] = Bson(id); 217 m_posts.insert(postbson); 218 return id; 219 } 220 221 void modifyPost(Post post) 222 { 223 assert(post.id.valid); 224 Bson update = post.toBson(); 225 m_posts.update(["_id": Bson(post.id)], update); 226 } 227 228 void deletePost(BsonObjectID id) 229 { 230 assert(id.valid); 231 m_posts.remove(["_id": Bson(id)]); 232 } 233 234 void addFile(string post_name, string file_name, InputStream contents) 235 { 236 import vibe.stream.operations : readAll; 237 struct I { 238 string postName; 239 string fileName; 240 } 241 m_postFiles.insert(PostFile(post_name, file_name, contents.readAll())); 242 } 243 244 string[] getFiles(string post_name) 245 { 246 import std.algorithm.iteration : map; 247 import std.array : array; 248 return m_postFiles.find(["postName": post_name], ["fileName": true]).map!(p => p["fileName"].get!string).array; 249 } 250 251 InputStream getFile(string post_name, string file_name) 252 { 253 auto f = m_postFiles.findOne!PostFile(["postName": post_name, "fileName": file_name]); 254 if (f.isNull) return null; 255 return new MemoryStream(f.contents); 256 } 257 258 void removeFile(string post_name, string file_name) 259 { 260 m_postFiles.remove(["postName": post_name, "fileName": file_name]); 261 } 262 263 private void upgradeComments(MongoDatabase db) 264 { 265 import diskuto.backend : StoredComment, CommentStatus; 266 import diskuto.backends.mongodb : MongoStruct; 267 268 auto comments = db["comments"]; 269 270 // Upgrade post contained comments to their collection 271 foreach( p; m_posts.find(["comments": ["$exists": true]], ["comments": 1]) ){ 272 foreach( c; p["comments"] ){ 273 c["_id"] = BsonObjectID.generate(); 274 c["postId"] = p["_id"]; 275 comments.insert(c); 276 } 277 m_posts.update(["_id": p["_id"]], ["$unset": ["comments": 1]]); 278 } 279 280 // Upgrade old comments to Diskuto format 281 foreach (c; comments.find(["postId": ["$exists": true]])) { 282 auto oldc = OldComment.fromBson(c); 283 StoredComment newc; 284 newc.id = oldc.id.toString(); 285 newc.status = oldc.isPublic ? CommentStatus.active : CommentStatus.disabled; 286 newc.topic = "vibelog-" ~ oldc.postId.toString(); 287 newc.author = "vibelog-..."; 288 newc.clientAddress = oldc.authorIP; 289 newc.name = oldc.authorName; 290 newc.email = oldc.authorMail; 291 newc.website = oldc.authorHomepage; 292 newc.text = oldc.content; 293 newc.time = oldc.date; 294 comments.update(["_id": c["_id"]], MongoStruct!StoredComment(newc)); 295 } 296 } 297 } 298 299 struct PostFile { 300 string postName; 301 string fileName; 302 ubyte[] contents; 303 } 304 305 306 final class OldComment { 307 BsonObjectID id; 308 BsonObjectID postId; 309 bool isPublic; 310 SysTime date; 311 int answerTo; 312 string authorName; 313 string authorMail; 314 string authorHomepage; 315 string authorIP; 316 string header; 317 string content; 318 319 static OldComment fromBson(Bson bson) 320 { 321 auto ret = new OldComment; 322 ret.id = cast(BsonObjectID)bson["_id"]; 323 ret.postId = cast(BsonObjectID)bson["postId"]; 324 ret.isPublic = cast(bool)bson["isPublic"]; 325 ret.date = SysTime.fromISOExtString(cast(string)bson["date"]); 326 ret.answerTo = cast(int)bson["answerTo"]; 327 ret.authorName = cast(string)bson["authorName"]; 328 ret.authorMail = cast(string)bson["authorMail"]; 329 ret.authorHomepage = cast(string)bson["authorHomepage"]; 330 ret.authorIP = bson["authorIP"].opt!string(); 331 ret.header = cast(string)bson["header"]; 332 ret.content = cast(string)bson["content"]; 333 return ret; 334 } 335 }