mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	becca conversion WIP
This commit is contained in:
		| @@ -3,7 +3,7 @@ const Note = require('../entities/note'); | ||||
| const NoteRevision = require('../services/becca/entities/note_revision.js'); | ||||
| const Branch = require('../entities/branch'); | ||||
| const Attribute = require('../entities/attribute'); | ||||
| const RecentNote = require('../entities/recent_note'); | ||||
| const RecentNote = require('../services/becca/entities/recent_note.js'); | ||||
| const ApiToken = require('../entities/api_token'); | ||||
| const cls = require('../services/cls'); | ||||
|  | ||||
|   | ||||
| @@ -1,28 +0,0 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const Entity = require('./entity'); | ||||
| const dateUtils = require('../services/date_utils'); | ||||
|  | ||||
| /** | ||||
|  * RecentNote represents recently visited note. | ||||
|  * | ||||
|  * @property {string} noteId | ||||
|  * @property {string} notePath | ||||
|  * @property {string} utcDateCreated | ||||
|  * | ||||
|  * @extends Entity | ||||
|  */ | ||||
| class RecentNote extends Entity { | ||||
|     static get entityName() { return "recent_notes"; } | ||||
|     static get primaryKeyName() { return "noteId"; } | ||||
|  | ||||
|     beforeSaving() { | ||||
|         if (!this.utcDateCreated) { | ||||
|             this.utcDateCreated = dateUtils.utcNowDateTime(); | ||||
|         } | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = RecentNote; | ||||
| @@ -5,7 +5,7 @@ const log = require('../../services/log'); | ||||
| const attributeService = require('../../services/attributes'); | ||||
| const repository = require('../../services/repository'); | ||||
| const Attribute = require('../../entities/attribute'); | ||||
| const becca = require("../../services/becca/becca.js"); | ||||
| const becca = require("../../services/becca/becca"); | ||||
|  | ||||
| function getEffectiveNoteAttributes(req) { | ||||
|     const note = becca.getNote(req.params.noteId); | ||||
|   | ||||
| @@ -6,6 +6,7 @@ const repository = require('../../services/repository'); | ||||
| const log = require('../../services/log'); | ||||
| const utils = require('../../services/utils'); | ||||
| const cls = require('../../services/cls'); | ||||
| const becca = require("../../services/becca/becca"); | ||||
|  | ||||
| function getAutocomplete(req) { | ||||
|     const query = req.query.query.trim(); | ||||
| @@ -41,7 +42,7 @@ function getRecentNotes(activeNoteId) { | ||||
|         params.push('%' + hoistedNoteId + '%'); | ||||
|     } | ||||
|  | ||||
|     const recentNotes = repository.getEntities(` | ||||
|     const recentNotes = becca.getRecentNotesFromQuery(` | ||||
|       SELECT  | ||||
|         recent_notes.*  | ||||
|       FROM  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ const fs = require('fs'); | ||||
| const { Readable } = require('stream'); | ||||
| const chokidar = require('chokidar'); | ||||
| const ws = require('../../services/ws'); | ||||
| const becca = require("../../services/becca/becca.js"); | ||||
| const becca = require("../../services/becca/becca"); | ||||
|  | ||||
| function updateFile(req) { | ||||
|     const {noteId} = req.params; | ||||
|   | ||||
| @@ -7,10 +7,10 @@ const noteRevisionService = require('../../services/note_revisions'); | ||||
| const utils = require('../../services/utils'); | ||||
| const sql = require('../../services/sql'); | ||||
| const path = require('path'); | ||||
| const becca = require("../../services/becca/becca.js"); | ||||
| const becca = require("../../services/becca/becca"); | ||||
|  | ||||
| function getNoteRevisions(req) { | ||||
|     return repository.getEntities(` | ||||
|     return becca.getNoteRevisionsFromQuery(` | ||||
|         SELECT note_revisions.*, | ||||
|                LENGTH(note_revision_contents.content) AS contentLength | ||||
|         FROM note_revisions | ||||
| @@ -107,7 +107,7 @@ function restoreNoteRevision(req) { | ||||
| } | ||||
|  | ||||
| function getEditedNotesOnDate(req) { | ||||
|     const notes = repository.getEntities(` | ||||
|     const noteIds = sql.getColumn(` | ||||
|         SELECT notes.* | ||||
|         FROM notes | ||||
|         WHERE noteId IN ( | ||||
| @@ -121,6 +121,8 @@ function getEditedNotesOnDate(req) { | ||||
|         ORDER BY isDeleted | ||||
|         LIMIT 50`, {date: req.params.date + '%'}); | ||||
|  | ||||
|     const notes = becca.getNotes(noteIds); | ||||
|  | ||||
|     for (const note of notes) { | ||||
|         const notePath = note.isDeleted ? null : beccaService.getNotePath(note.noteId); | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ const log = require('../../services/log'); | ||||
| const TaskContext = require('../../services/task_context'); | ||||
| const fs = require('fs'); | ||||
| const noteRevisionService = require("../../services/note_revisions.js"); | ||||
| const becca = require("../../services/becca/becca.js"); | ||||
| const becca = require("../../services/becca/becca"); | ||||
|  | ||||
| function getNote(req) { | ||||
|     const noteId = req.params.noteId; | ||||
| @@ -159,7 +159,8 @@ function getRelationMap(req) { | ||||
|  | ||||
|     console.log("displayRelations", displayRelations); | ||||
|  | ||||
|     const notes = repository.getEntities(`SELECT * FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); | ||||
|     const foundNoteIds = sql.getColumn(`SELECT noteId FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); | ||||
|     const notes = becca.getNotes(foundNoteIds); | ||||
|  | ||||
|     for (const note of notes) { | ||||
|         resp.noteTitles[note.noteId] = note.title; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const RecentNote = require('../../entities/recent_note'); | ||||
| const RecentNote = require('../../services/becca/entities/recent_note.js'); | ||||
| const sql = require('../../services/sql'); | ||||
| const dateUtils = require('../../services/date_utils'); | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ const log = require('../../services/log'); | ||||
| const scriptService = require('../../services/script'); | ||||
| const searchService = require('../../services/search/services/search'); | ||||
| const noteRevisionService = require("../../services/note_revisions.js"); | ||||
| const {formatAttrForSearch} = require("../../services/attribute_formatter.js"); | ||||
|  | ||||
| async function searchFromNoteInt(note) { | ||||
|     let searchResultNoteIds; | ||||
| @@ -267,51 +268,6 @@ function getRelatedNotes(req) { | ||||
|     }; | ||||
| } | ||||
|  | ||||
| function formatAttrForSearch(attr, searchWithValue) { | ||||
|     let searchStr = ''; | ||||
|  | ||||
|     if (attr.type === 'label') { | ||||
|         searchStr += '#'; | ||||
|     } | ||||
|     else if (attr.type === 'relation') { | ||||
|         searchStr += '~'; | ||||
|     } | ||||
|     else { | ||||
|         throw new Error(`Unrecognized attribute type ${JSON.stringify(attr)}`); | ||||
|     } | ||||
|  | ||||
|     searchStr += attr.name; | ||||
|  | ||||
|     if (searchWithValue && attr.value) { | ||||
|         if (attr.type === 'relation') { | ||||
|             searchStr += ".noteId"; | ||||
|         } | ||||
|  | ||||
|         searchStr += '='; | ||||
|         searchStr += formatValue(attr.value); | ||||
|     } | ||||
|  | ||||
|     return searchStr; | ||||
| } | ||||
|  | ||||
| function formatValue(val) { | ||||
|     if (!/[^\w_-]/.test(val)) { | ||||
|         return val; | ||||
|     } | ||||
|     else if (!val.includes('"')) { | ||||
|         return '"' + val + '"'; | ||||
|     } | ||||
|     else if (!val.includes("'")) { | ||||
|         return "'" + val + "'"; | ||||
|     } | ||||
|     else if (!val.includes("`")) { | ||||
|         return "`" + val + "`"; | ||||
|     } | ||||
|     else { | ||||
|         return '"' + val.replace(/"/g, '\\"') + '"'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     searchFromNote, | ||||
|     searchAndExecute, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const similarityService = require('../../services/becca/similarity.js'); | ||||
| const becca = require("../../services/becca/becca.js"); | ||||
| const becca = require("../../services/becca/becca"); | ||||
|  | ||||
| async function getSimilarNotes(req) { | ||||
|     const noteId = req.params.noteId; | ||||
|   | ||||
| @@ -3,13 +3,17 @@ const log = require('../services/log'); | ||||
| const fileUploadService = require('./api/files.js'); | ||||
| const scriptService = require('../services/script'); | ||||
| const cls = require('../services/cls'); | ||||
| const sql = require("../services/sql"); | ||||
| const becca = require("../services/becca/becca"); | ||||
|  | ||||
| async function handleRequest(req, res) { | ||||
|     // express puts content after first slash into 0 index element | ||||
|  | ||||
|     const path = req.params.path + req.params[0]; | ||||
|  | ||||
|     const attrs = repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')"); | ||||
|     const attributeIds = sql.getColumn("SELECT attributeId FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')"); | ||||
|  | ||||
|     const attrs = attributeIds.map(attrId => becca.getAttribute(attrId)); | ||||
|  | ||||
|     for (const attr of attrs) { | ||||
|         if (!attr.value.trim()) { | ||||
|   | ||||
							
								
								
									
										50
									
								
								src/services/attribute_formatter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/services/attribute_formatter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| "use strict" | ||||
|  | ||||
| function formatAttrForSearch(attr, searchWithValue) { | ||||
|     let searchStr = ''; | ||||
|  | ||||
|     if (attr.type === 'label') { | ||||
|         searchStr += '#'; | ||||
|     } | ||||
|     else if (attr.type === 'relation') { | ||||
|         searchStr += '~'; | ||||
|     } | ||||
|     else { | ||||
|         throw new Error(`Unrecognized attribute type ${JSON.stringify(attr)}`); | ||||
|     } | ||||
|  | ||||
|     searchStr += attr.name; | ||||
|  | ||||
|     if (searchWithValue && attr.value) { | ||||
|         if (attr.type === 'relation') { | ||||
|             searchStr += ".noteId"; | ||||
|         } | ||||
|  | ||||
|         searchStr += '='; | ||||
|         searchStr += formatValue(attr.value); | ||||
|     } | ||||
|  | ||||
|     return searchStr; | ||||
| } | ||||
|  | ||||
| function formatValue(val) { | ||||
|     if (!/[^\w_-]/.test(val)) { | ||||
|         return val; | ||||
|     } | ||||
|     else if (!val.includes('"')) { | ||||
|         return '"' + val + '"'; | ||||
|     } | ||||
|     else if (!val.includes("'")) { | ||||
|         return "'" + val + "'"; | ||||
|     } | ||||
|     else if (!val.includes("`")) { | ||||
|         return "`" + val + "`"; | ||||
|     } | ||||
|     else { | ||||
|         return '"' + val.replace(/"/g, '\\"') + '"'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     formatAttrForSearch | ||||
| } | ||||
| @@ -1,9 +1,10 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const repository = require('./repository'); | ||||
| const searchService = require('./search/services/search'); | ||||
| const sql = require('./sql'); | ||||
| const becca = require('./becca/becca.js'); | ||||
| const Attribute = require('../entities/attribute'); | ||||
| const {formatAttrForSearch} = require("./attribute_formatter.js"); | ||||
|  | ||||
| const ATTRIBUTE_TYPES = [ 'label', 'relation' ]; | ||||
|  | ||||
| @@ -59,16 +60,7 @@ const BUILTIN_ATTRIBUTES = [ | ||||
| ]; | ||||
|  | ||||
| function getNotesWithLabel(name, value) { | ||||
|     let valueCondition = ""; | ||||
|     let params = [name]; | ||||
|  | ||||
|     if (value !== undefined) { | ||||
|         valueCondition = " AND attributes.value = ?"; | ||||
|         params.push(value); | ||||
|     } | ||||
|  | ||||
|     return repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) | ||||
|           WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? ${valueCondition} ORDER BY position`, params); | ||||
|     return searchService.findNotes(formatAttrForSearch({type: 'label', name, value}, true)); | ||||
| } | ||||
|  | ||||
| function getNoteIdsWithLabels(names) { | ||||
|   | ||||
| @@ -14,7 +14,7 @@ const cloningService = require('./cloning'); | ||||
| const appInfo = require('./app_info'); | ||||
| const searchService = require('./search/services/search'); | ||||
| const SearchContext = require("./search/search_context.js"); | ||||
| const becca = require("./becca/becca.js"); | ||||
| const becca = require("./becca/becca"); | ||||
|  | ||||
| /** | ||||
|  * This is the main backend API interface for scripts. It's published in the local "api" object. | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| const sql = require("../sql.js"); | ||||
| const NoteRevision = require("./entities/note_revision.js"); | ||||
| const RecentNote = require("./entities/recent_note.js"); | ||||
|  | ||||
| class Becca { | ||||
|     constructor() { | ||||
| @@ -94,6 +95,18 @@ class Becca { | ||||
|  | ||||
|         return this[camelCaseEntityName][entityId]; | ||||
|     } | ||||
|  | ||||
|     getRecentNotesFromQuery(query, params = []) { | ||||
|         const rows = sql.getRows(query, params); | ||||
|  | ||||
|         return rows.map(row => new RecentNote(row)); | ||||
|     } | ||||
|  | ||||
|     getNoteRevisionsFromQuery(query, params = []) { | ||||
|         const rows = sql.getRows(query, params); | ||||
|  | ||||
|         return rows.map(row => new NoteRevision(row)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| const becca = new Becca(); | ||||
|   | ||||
							
								
								
									
										22
									
								
								src/services/becca/entities/recent_note.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/services/becca/entities/recent_note.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const dateUtils = require('../../date_utils.js'); | ||||
| const AbstractEntity = require("./abstract_entity.js"); | ||||
|  | ||||
| /** | ||||
|  * RecentNote represents recently visited note. | ||||
|  */ | ||||
| class RecentNote extends AbstractEntity { | ||||
|     static get entityName() { return "recent_notes"; } | ||||
|     static get primaryKeyName() { return "noteId"; } | ||||
|  | ||||
|     constructor(row) { | ||||
|         super(); | ||||
|  | ||||
|         this.noteId = row.noteId; | ||||
|         this.notePath = row.notePath; | ||||
|         this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = RecentNote; | ||||
| @@ -8,7 +8,7 @@ const repository = require('./repository'); | ||||
| const Branch = require('../entities/branch'); | ||||
| const TaskContext = require("./task_context.js"); | ||||
| const utils = require('./utils'); | ||||
| const becca = require("./becca/becca.js"); | ||||
| const becca = require("./becca/becca"); | ||||
|  | ||||
| function cloneNoteToParent(noteId, parentBranchId, prefix) { | ||||
|     const parentBranch = becca.getBranch(parentBranchId); | ||||
|   | ||||
| @@ -13,7 +13,7 @@ const Branch = require('../entities/branch'); | ||||
| const dateUtils = require('./date_utils'); | ||||
| const attributeService = require('./attributes'); | ||||
| const noteRevisionService = require('./note_revisions'); | ||||
| const becca = require("./becca/becca.js"); | ||||
| const becca = require("./becca/becca"); | ||||
|  | ||||
| class ConsistencyChecks { | ||||
|     constructor(autoFix) { | ||||
| @@ -244,13 +244,15 @@ class ConsistencyChecks { | ||||
|                     HAVING COUNT(1) > 1`, | ||||
|             ({noteId, parentNoteId}) => { | ||||
|                 if (this.autoFix) { | ||||
|                     const branches = repository.getEntities( | ||||
|                             `SELECT * | ||||
|                     const branchIds = sql.getColumn( | ||||
|                             `SELECT branchId | ||||
|                              FROM branches | ||||
|                              WHERE noteId = ? | ||||
|                                and parentNoteId = ? | ||||
|                                and isDeleted = 0`, [noteId, parentNoteId]); | ||||
|  | ||||
|                     const branches = branchIds.map(branchId => becca.getBranch(branchId)); | ||||
|  | ||||
|                     // it's not necessarily "original" branch, it's just the only one which will survive | ||||
|                     const origBranch = branches[0]; | ||||
|  | ||||
| @@ -359,11 +361,13 @@ class ConsistencyChecks { | ||||
|                       AND branches.isDeleted = 0`, | ||||
|             ({parentNoteId}) => { | ||||
|                 if (this.autoFix) { | ||||
|                     const branches = repository.getEntities(`SELECT * | ||||
|                     const branchIds = sql.getColumn(`SELECT branchId | ||||
|                                                                    FROM branches | ||||
|                                                                    WHERE isDeleted = 0 | ||||
|                                                                      AND parentNoteId = ?`, [parentNoteId]); | ||||
|  | ||||
|                     const branches = branchIds.map(branchId => becca.getBranch(branchId)); | ||||
|  | ||||
|                     for (const branch of branches) { | ||||
|                         branch.parentNoteId = 'root'; | ||||
|                         branch.save(); | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| const repository = require("../repository"); | ||||
| const utils = require('../utils'); | ||||
| const becca = require("../becca/becca.js"); | ||||
| const becca = require("../becca/becca"); | ||||
|  | ||||
| function exportToOpml(taskContext, branch, version, res) { | ||||
|     if (!['1.0', '2.0'].includes(version)) { | ||||
|   | ||||
| @@ -6,11 +6,8 @@ const entityChangesService = require('./entity_changes.js'); | ||||
| const eventService = require('./events'); | ||||
| const repository = require('./repository'); | ||||
| const cls = require('../services/cls'); | ||||
| const Note = require('../entities/note'); | ||||
| const BeccaNote = require('../services/becca/entities/note.js'); | ||||
| const Branch = require('../entities/branch'); | ||||
| const BeccaBranch = require('../services/becca/entities/branch.js'); | ||||
| const Attribute = require('../entities/attribute'); | ||||
| const BeccaAttribute = require('../services/becca/entities/attribute.js'); | ||||
| const protectedSessionService = require('../services/protected_session'); | ||||
| const log = require('../services/log'); | ||||
| @@ -617,14 +614,14 @@ function undeleteBranch(branch, deleteId, taskContext) { | ||||
|         note.isDeleted = false; | ||||
|         note.save(); | ||||
|  | ||||
|         const attrs = repository.getEntities(` | ||||
|                 SELECT * FROM attributes  | ||||
|         const attributeIds = sql.getColumn(` | ||||
|                 SELECT attributeId FROM attributes  | ||||
|                 WHERE isDeleted = 1  | ||||
|                   AND deleteId = ?  | ||||
|                   AND (noteId = ?  | ||||
|                            OR (type = 'relation' AND value = ?))`, [deleteId, note.noteId, note.noteId]); | ||||
|  | ||||
|         for (const attr of attrs) { | ||||
|         for (const attr of attributeIds) { | ||||
|             attr.isDeleted = false; | ||||
|             attr.save(); | ||||
|         } | ||||
| @@ -646,14 +643,16 @@ function undeleteBranch(branch, deleteId, taskContext) { | ||||
|  * @return return deleted branches of an undeleted parent note | ||||
|  */ | ||||
| function getUndeletedParentBranches(noteId, deleteId) { | ||||
|     return repository.getEntities(` | ||||
|                     SELECT branches.* | ||||
|     const branchIds = sql.getColumn(` | ||||
|                     SELECT branches.branchId | ||||
|                     FROM branches | ||||
|                     JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId | ||||
|                     WHERE branches.noteId = ? | ||||
|                       AND branches.isDeleted = 1 | ||||
|                       AND branches.deleteId = ? | ||||
|                       AND parentNote.isDeleted = 0`, [noteId, deleteId]); | ||||
|  | ||||
|     return branchIds.map(branchId => becca.getBranch(branchId)); | ||||
| } | ||||
|  | ||||
| function scanForLinks(note) { | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| const scriptService = require('./script'); | ||||
| const repository = require('./repository'); | ||||
| const cls = require('./cls'); | ||||
| const sqlInit = require('./sql_init'); | ||||
| const config = require('./config'); | ||||
| const log = require('./log'); | ||||
| const sql = require("./sql"); | ||||
| const becca = require("./becca/becca"); | ||||
|  | ||||
| function getRunAtHours(note) { | ||||
|     try { | ||||
| @@ -17,8 +18,9 @@ function getRunAtHours(note) { | ||||
| } | ||||
|  | ||||
| function runNotesWithLabel(runAttrValue) { | ||||
|     const notes = repository.getEntities(` | ||||
|         SELECT notes.*  | ||||
|     // TODO: should be refactored into becca search | ||||
|     const noteIds = sql.getColumn(` | ||||
|         SELECT notes.noteId  | ||||
|         FROM notes  | ||||
|         JOIN attributes ON attributes.noteId = notes.noteId | ||||
|                        AND attributes.isDeleted = 0 | ||||
| @@ -29,6 +31,8 @@ function runNotesWithLabel(runAttrValue) { | ||||
|           notes.type = 'code' | ||||
|           AND notes.isDeleted = 0`, [runAttrValue]); | ||||
|  | ||||
|     const notes = becca.getNotes(noteIds); | ||||
|  | ||||
|     const instanceName = config.General ? config.General.instanceName : null; | ||||
|     const currentHours = new Date().getHours(); | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ const ScriptContext = require('./script_context'); | ||||
| const repository = require('./repository'); | ||||
| const cls = require('./cls'); | ||||
| const log = require('./log'); | ||||
| const becca = require("./becca/becca.js"); | ||||
| const becca = require("./becca/becca"); | ||||
|  | ||||
| async function executeNote(note, apiParams) { | ||||
|     if (!note.isJavaScript() || note.getScriptEnv() !== 'backend' || !note.isContentAvailable) { | ||||
|   | ||||
| @@ -7,7 +7,7 @@ const syncOptions = require('./sync_options'); | ||||
| const request = require('./request'); | ||||
| const appInfo = require('./app_info'); | ||||
| const utils = require('./utils'); | ||||
| const becca = require("./becca/becca.js"); | ||||
| const becca = require("./becca/becca"); | ||||
|  | ||||
| async function hasSyncServerSchemaAndSeed() { | ||||
|     const response = await requestToSyncServer('GET', '/api/setup/status'); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user