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.data.bson; 8 import vibe.db.mongo.mongo; 9 import vibe.mail.smtp; 10 import vibe.stream.memory; 11 12 import std.exception; 13 import std.variant; 14 15 16 final class MongoDBController : DBController { 17 private { 18 MongoCollection m_configs; 19 MongoCollection m_users; 20 MongoCollection m_posts; 21 MongoCollection m_postFiles; 22 void delegate()[] m_onConfigChange; 23 } 24 25 this(string db_url) 26 { 27 string database = "vibelog"; 28 MongoClientSettings dbsettings; 29 if (parseMongoDBUrl(dbsettings, db_url)) 30 database = dbsettings.database; 31 32 auto db = connectMongoDB(db_url).getDatabase(database); 33 m_configs = db["configs"]; 34 m_users = db["users"]; 35 m_posts = db["posts"]; 36 m_postFiles = db["postFiles"]; 37 38 upgradeComments(db); 39 } 40 41 Config getConfig(string name, bool createdefault = false) 42 { 43 auto configbson = m_configs.findOne(["name": Bson(name)]); 44 if( !configbson.isNull() ) 45 return Config.fromBson(configbson); 46 enforce(createdefault, "Configuration does not exist."); 47 auto cfg = new Config; 48 cfg.name = name; 49 m_configs.insert(cfg.toBson()); 50 return cfg; 51 } 52 53 void setConfig(Config cfg) 54 { 55 Bson update = cfg.toBson(); 56 m_configs.update(["name": Bson(cfg.name)], update); 57 foreach (d; m_onConfigChange) d(); 58 } 59 60 void invokeOnConfigChange(void delegate() del) 61 { 62 m_onConfigChange ~= del; 63 } 64 65 void deleteConfig(string name) 66 { 67 m_configs.remove(["name": Bson(name)]); 68 } 69 70 Config[] getAllConfigs() 71 { 72 Bson[string] query; 73 Config[] ret; 74 foreach( config; m_configs.find(query) ){ 75 auto c = Config.fromBson(config); 76 ret ~= c; 77 } 78 return ret; 79 } 80 81 User[string] getAllUsers() 82 { 83 import vibelog.internal.passwordhash : generatePasswordHash; 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 = generatePasswordHash("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 try { 114 auto id = BsonObjectID.fromHexString(name); 115 logDebug("%s <-> %s", name, id.toString()); 116 assert(id.toString() == name); 117 userbson = m_users.findOne(["_id": Bson(id)]); 118 } catch (Exception e) { 119 return null; 120 } 121 } 122 //auto userbson = m_users.findOne(Bson(["name" : Bson(name)])); 123 return User.fromBson(userbson); 124 } 125 126 User getUserByEmail(string email) 127 { 128 auto userbson = m_users.findOne(["email": Bson(email)]); 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(["date" : Bson(-1)])]; 167 foreach (idx, post; m_posts.find(query, null, QueryFlags.None, nskip).byPair) { 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(["date" : Bson(-1)])]; 179 foreach (idx, post; m_posts.find(query, null, QueryFlags.None, nskip).byPair) { 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(["date" : Bson(-1)])]; 189 foreach (idx, post; m_posts.find(extquery, null, QueryFlags.None, nskip).byPair) { 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, in ubyte[] 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, cast(ubyte[])contents)); 246 } 247 248 string[] getFiles(string post_name) 249 { 250 import std.algorithm.iteration : map; 251 import std.array : array; 252 return m_postFiles.find(["postName": post_name], ["fileName": true]).map!(p => p["fileName"].get!string).array; 253 } 254 255 InputStream getFile(string post_name, string file_name) 256 { 257 auto f = m_postFiles.findOne!PostFile(["postName": post_name, "fileName": file_name]); 258 if (f.isNull) return null; 259 return createMemoryStream(f.get.contents); 260 } 261 262 void removeFile(string post_name, string file_name) 263 { 264 m_postFiles.remove(["postName": post_name, "fileName": file_name]); 265 } 266 267 private void upgradeComments(MongoDatabase db) 268 { 269 import diskuto.commentstore : StoredComment, CommentStatus; 270 import diskuto.commentstores.mongodb : MongoStruct; 271 272 auto comments = db["comments"]; 273 274 // Upgrade post contained comments to their collection 275 foreach( p; m_posts.find(["comments": ["$exists": true]], ["comments": 1]) ){ 276 foreach( c; p["comments"] ){ 277 c["_id"] = BsonObjectID.generate(); 278 c["postId"] = p["_id"]; 279 comments.insert(c); 280 } 281 m_posts.update(["_id": p["_id"]], ["$unset": ["comments": 1]]); 282 } 283 284 // Upgrade old comments to Diskuto format 285 foreach (c; comments.find(["postId": ["$exists": true]])) { 286 auto oldc = OldComment.fromBson(c); 287 StoredComment newc; 288 newc.id = oldc.id.toString(); 289 newc.status = oldc.isPublic ? CommentStatus.active : CommentStatus.disabled; 290 newc.topic = "vibelog-" ~ oldc.postId.toString(); 291 newc.author = "vibelog-..."; 292 newc.clientAddress = oldc.authorIP; 293 newc.name = oldc.authorName; 294 newc.email = oldc.authorMail; 295 newc.website = oldc.authorHomepage; 296 newc.text = oldc.content; 297 newc.time = oldc.date; 298 comments.update(["_id": c["_id"]], MongoStruct!StoredComment(newc)); 299 } 300 } 301 } 302 303 struct PostFile { 304 string postName; 305 string fileName; 306 ubyte[] contents; 307 } 308 309 310 final class OldComment { 311 BsonObjectID id; 312 BsonObjectID postId; 313 bool isPublic; 314 SysTime date; 315 int answerTo; 316 string authorName; 317 string authorMail; 318 string authorHomepage; 319 string authorIP; 320 string header; 321 string content; 322 323 static OldComment fromBson(Bson bson) 324 { 325 auto ret = new OldComment; 326 ret.id = cast(BsonObjectID)bson["_id"]; 327 ret.postId = cast(BsonObjectID)bson["postId"]; 328 ret.isPublic = cast(bool)bson["isPublic"]; 329 ret.date = SysTime.fromISOExtString(cast(string)bson["date"]); 330 ret.answerTo = cast(int)bson["answerTo"]; 331 ret.authorName = cast(string)bson["authorName"]; 332 ret.authorMail = cast(string)bson["authorMail"]; 333 ret.authorHomepage = cast(string)bson["authorHomepage"]; 334 ret.authorIP = bson["authorIP"].opt!string(); 335 ret.header = cast(string)bson["header"]; 336 ret.content = cast(string)bson["content"]; 337 return ret; 338 } 339 }