add: journals/notes database interfaces
This commit is contained in:
parent
0f48a46c40
commit
102ea0ee35
14 changed files with 386 additions and 6 deletions
|
@ -112,7 +112,19 @@
|
||||||
("class" "button")
|
("class" "button")
|
||||||
("data-turbo" "false")
|
("data-turbo" "false")
|
||||||
(icon (text "rabbit"))
|
(icon (text "rabbit"))
|
||||||
(str (text "general:link.reference"))))))
|
(str (text "general:link.reference")))
|
||||||
|
|
||||||
|
(a
|
||||||
|
("href" "{{ config.policies.terms_of_service }}")
|
||||||
|
("class" "button")
|
||||||
|
(icon (text "heart-handshake"))
|
||||||
|
(text "Terms of service"))
|
||||||
|
|
||||||
|
(a
|
||||||
|
("href" "{{ config.policies.privacy }}")
|
||||||
|
("class" "button")
|
||||||
|
(icon (text "cookie"))
|
||||||
|
(text "Privacy policy")))))
|
||||||
(text "{%- endif %}")))
|
(text "{%- endif %}")))
|
||||||
(text "{%- endmacro %}")
|
(text "{%- endmacro %}")
|
||||||
|
|
||||||
|
|
|
@ -585,7 +585,9 @@
|
||||||
(li
|
(li
|
||||||
(text "Add unlimited users to stacks"))
|
(text "Add unlimited users to stacks"))
|
||||||
(li
|
(li
|
||||||
(text "Increased proxied image size")))
|
(text "Increased proxied image size"))
|
||||||
|
(li
|
||||||
|
(text "Create infinite journals")))
|
||||||
(a
|
(a
|
||||||
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
|
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
|
||||||
("class" "button")
|
("class" "button")
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
globalThis.ns_config = {
|
globalThis.ns_config = {
|
||||||
root: \"/js/\",
|
root: \"/js/\",
|
||||||
verbose: globalThis.ns_verbose,
|
verbose: globalThis.ns_verbose,
|
||||||
version: \"cache-breaker-{{ random_cache_breaker }}\",
|
version: \"tetratto-{{ random_cache_breaker }}\",
|
||||||
};
|
};
|
||||||
|
|
||||||
globalThis._app_base = {
|
globalThis._app_base = {
|
||||||
|
@ -38,8 +38,8 @@
|
||||||
globalThis.BUILD_CODE = \"{{ random_cache_breaker }}\";
|
globalThis.BUILD_CODE = \"{{ random_cache_breaker }}\";
|
||||||
</script>")
|
</script>")
|
||||||
|
|
||||||
(script ("src" "/js/loader.js" ))
|
(script ("src" "/js/loader.js?v=tetratto-{{ random_cache_breaker }}" ))
|
||||||
(script ("src" "/js/atto.js" ))
|
(script ("src" "/js/atto.js?v=tetratto-{{ random_cache_breaker }}" ))
|
||||||
|
|
||||||
(meta ("name" "theme-color") ("content" "{{ config.color }}"))
|
(meta ("name" "theme-color") ("content" "{{ config.color }}"))
|
||||||
(meta ("name" "description") ("content" "{{ config.description }}"))
|
(meta ("name" "description") ("content" "{{ config.description }}"))
|
||||||
|
|
|
@ -36,6 +36,8 @@ impl DataManager {
|
||||||
execute!(&conn, common::CREATE_TABLE_POLLVOTES).unwrap();
|
execute!(&conn, common::CREATE_TABLE_POLLVOTES).unwrap();
|
||||||
execute!(&conn, common::CREATE_TABLE_APPS).unwrap();
|
execute!(&conn, common::CREATE_TABLE_APPS).unwrap();
|
||||||
execute!(&conn, common::CREATE_TABLE_STACKBLOCKS).unwrap();
|
execute!(&conn, common::CREATE_TABLE_STACKBLOCKS).unwrap();
|
||||||
|
execute!(&conn, common::CREATE_TABLE_JOURNALS).unwrap();
|
||||||
|
execute!(&conn, common::CREATE_TABLE_NOTES).unwrap();
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.1
|
.1
|
||||||
|
|
|
@ -23,3 +23,5 @@ pub const CREATE_TABLE_POLLS: &str = include_str!("./sql/create_polls.sql");
|
||||||
pub const CREATE_TABLE_POLLVOTES: &str = include_str!("./sql/create_pollvotes.sql");
|
pub const CREATE_TABLE_POLLVOTES: &str = include_str!("./sql/create_pollvotes.sql");
|
||||||
pub const CREATE_TABLE_APPS: &str = include_str!("./sql/create_apps.sql");
|
pub const CREATE_TABLE_APPS: &str = include_str!("./sql/create_apps.sql");
|
||||||
pub const CREATE_TABLE_STACKBLOCKS: &str = include_str!("./sql/create_stackblocks.sql");
|
pub const CREATE_TABLE_STACKBLOCKS: &str = include_str!("./sql/create_stackblocks.sql");
|
||||||
|
pub const CREATE_TABLE_JOURNALS: &str = include_str!("./sql/create_journals.sql");
|
||||||
|
pub const CREATE_TABLE_NOTES: &str = include_str!("./sql/create_notes.sql");
|
||||||
|
|
7
crates/core/src/database/drivers/sql/create_journals.sql
Normal file
7
crates/core/src/database/drivers/sql/create_journals.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS channels (
|
||||||
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
created BIGINT NOT NULL,
|
||||||
|
owner BIGINT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
view TEXT NOT NULL
|
||||||
|
)
|
9
crates/core/src/database/drivers/sql/create_notes.sql
Normal file
9
crates/core/src/database/drivers/sql/create_notes.sql
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS channels (
|
||||||
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
created BIGINT NOT NULL,
|
||||||
|
owner BIGINT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
journal BIGINT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
edited BIGINT NOT NULL
|
||||||
|
)
|
141
crates/core/src/database/journals.rs
Normal file
141
crates/core/src/database/journals.rs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
use oiseau::cache::Cache;
|
||||||
|
use crate::{
|
||||||
|
model::{
|
||||||
|
auth::User,
|
||||||
|
permissions::FinePermission,
|
||||||
|
journals::{Journal, JournalViewPermission},
|
||||||
|
Error, Result,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use crate::{auto_method, DataManager};
|
||||||
|
use oiseau::{PostgresRow, execute, get, query_rows, params};
|
||||||
|
|
||||||
|
impl DataManager {
|
||||||
|
/// Get a [`Journal`] from an SQL row.
|
||||||
|
pub(crate) fn get_journal_from_row(x: &PostgresRow) -> Journal {
|
||||||
|
Journal {
|
||||||
|
id: get!(x->0(i64)) as usize,
|
||||||
|
created: get!(x->1(i64)) as usize,
|
||||||
|
owner: get!(x->2(i64)) as usize,
|
||||||
|
title: get!(x->3(String)),
|
||||||
|
view: serde_json::from_str(&get!(x->4(String))).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_method!(get_journal_by_id(usize as i64)@get_journal_from_row -> "SELECT * FROM journals WHERE id = $1" --name="journal" --returns=Journal --cache-key-tmpl="atto.journal:{}");
|
||||||
|
|
||||||
|
/// Get all journals by user.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `id` - the ID of the user to fetch journals for
|
||||||
|
pub async fn get_journals_by_user(&self, id: usize) -> Result<Vec<Journal>> {
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = query_rows!(
|
||||||
|
&conn,
|
||||||
|
"SELECT * FROM journals WHERE owner = $1 ORDER BY name ASC",
|
||||||
|
&[&(id as i64)],
|
||||||
|
|x| { Self::get_journal_from_row(x) }
|
||||||
|
);
|
||||||
|
|
||||||
|
if res.is_err() {
|
||||||
|
return Err(Error::GeneralNotFound("journal".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAXIMUM_FREE_JOURNALS: usize = 15;
|
||||||
|
|
||||||
|
/// Create a new journal in the database.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `data` - a mock [`Journal`] object to insert
|
||||||
|
pub async fn create_journal(&self, data: Journal) -> Result<Journal> {
|
||||||
|
// check values
|
||||||
|
if data.title.len() < 2 {
|
||||||
|
return Err(Error::DataTooShort("title".to_string()));
|
||||||
|
} else if data.title.len() > 32 {
|
||||||
|
return Err(Error::DataTooLong("title".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// check number of journals
|
||||||
|
let owner = self.get_user_by_id(data.owner).await?;
|
||||||
|
|
||||||
|
if !owner.permissions.check(FinePermission::SUPPORTER) {
|
||||||
|
let journals = self.get_journals_by_user(data.owner).await?;
|
||||||
|
|
||||||
|
if journals.len() >= Self::MAXIMUM_FREE_JOURNALS {
|
||||||
|
return Err(Error::MiscError(
|
||||||
|
"You already have the maximum number of journals you can have".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = execute!(
|
||||||
|
&conn,
|
||||||
|
"INSERT INTO journals VALUES ($1, $2, $3, $4, $5)",
|
||||||
|
params![
|
||||||
|
&(data.id as i64),
|
||||||
|
&(data.created as i64),
|
||||||
|
&(data.owner as i64),
|
||||||
|
&data.title,
|
||||||
|
&serde_json::to_string(&data.view).unwrap(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_journal(&self, id: usize, user: &User) -> Result<()> {
|
||||||
|
let journal = self.get_journal_by_id(id).await?;
|
||||||
|
|
||||||
|
// check user permission
|
||||||
|
if user.id != journal.owner && !user.permissions.check(FinePermission::MANAGE_JOURNALS) {
|
||||||
|
return Err(Error::NotAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = execute!(&conn, "DELETE FROM journals WHERE id = $1", &[&(id as i64)]);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete notes
|
||||||
|
let res = execute!(
|
||||||
|
&conn,
|
||||||
|
"DELETE FROM notes WHERE journal = $1",
|
||||||
|
&[&(id as i64)]
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
self.0.1.remove(format!("atto.journal:{}", id)).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_method!(update_journal_title(&str)@get_journal_by_id:MANAGE_JOURNALS -> "UPDATE journals SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.journal:{}");
|
||||||
|
auto_method!(update_journal_view(JournalViewPermission)@get_journal_by_id:MANAGE_JOURNALS -> "UPDATE journals SET privacy = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.journal:{}");
|
||||||
|
}
|
|
@ -10,8 +10,10 @@ mod drivers;
|
||||||
mod emojis;
|
mod emojis;
|
||||||
mod ipbans;
|
mod ipbans;
|
||||||
mod ipblocks;
|
mod ipblocks;
|
||||||
|
mod journals;
|
||||||
mod memberships;
|
mod memberships;
|
||||||
mod messages;
|
mod messages;
|
||||||
|
mod notes;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
mod polls;
|
mod polls;
|
||||||
mod pollvotes;
|
mod pollvotes;
|
||||||
|
|
124
crates/core/src/database/notes.rs
Normal file
124
crates/core/src/database/notes.rs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
use oiseau::cache::Cache;
|
||||||
|
use crate::model::{auth::User, journals::Note, permissions::FinePermission, Error, Result};
|
||||||
|
use crate::{auto_method, DataManager};
|
||||||
|
use oiseau::{PostgresRow, execute, get, query_rows, params};
|
||||||
|
|
||||||
|
impl DataManager {
|
||||||
|
/// Get a [`Note`] from an SQL row.
|
||||||
|
pub(crate) fn get_note_from_row(x: &PostgresRow) -> Note {
|
||||||
|
Note {
|
||||||
|
id: get!(x->0(i64)) as usize,
|
||||||
|
created: get!(x->1(i64)) as usize,
|
||||||
|
owner: get!(x->2(i64)) as usize,
|
||||||
|
title: get!(x->3(String)),
|
||||||
|
journal: get!(x->4(i64)) as usize,
|
||||||
|
content: get!(x->5(String)),
|
||||||
|
edited: get!(x->6(i64)) as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_method!(get_note_by_id(usize as i64)@get_note_from_row -> "SELECT * FROM notes WHERE id = $1" --name="note" --returns=Note --cache-key-tmpl="atto.note:{}");
|
||||||
|
|
||||||
|
/// Get all notes by journal.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `id` - the ID of the journal to fetch notes for
|
||||||
|
pub async fn get_notes_by_journal(&self, id: usize) -> Result<Vec<Note>> {
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = query_rows!(
|
||||||
|
&conn,
|
||||||
|
"SELECT * FROM notes WHERE journal = $1 ORDER BY edited",
|
||||||
|
&[&(id as i64)],
|
||||||
|
|x| { Self::get_note_from_row(x) }
|
||||||
|
);
|
||||||
|
|
||||||
|
if res.is_err() {
|
||||||
|
return Err(Error::GeneralNotFound("note".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new note in the database.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `data` - a mock [`Note`] object to insert
|
||||||
|
pub async fn create_note(&self, data: Note) -> Result<Note> {
|
||||||
|
// check values
|
||||||
|
if data.title.len() < 2 {
|
||||||
|
return Err(Error::DataTooShort("title".to_string()));
|
||||||
|
} else if data.title.len() > 64 {
|
||||||
|
return Err(Error::DataTooLong("title".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.content.len() < 2 {
|
||||||
|
return Err(Error::DataTooShort("content".to_string()));
|
||||||
|
} else if data.content.len() > 16384 {
|
||||||
|
return Err(Error::DataTooLong("content".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = execute!(
|
||||||
|
&conn,
|
||||||
|
"INSERT INTO notes VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||||
|
params![
|
||||||
|
&(data.id as i64),
|
||||||
|
&(data.created as i64),
|
||||||
|
&(data.owner as i64),
|
||||||
|
&data.title,
|
||||||
|
&(data.journal as i64),
|
||||||
|
&data.content,
|
||||||
|
&(data.edited as i64),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_note(&self, id: usize, user: &User) -> Result<()> {
|
||||||
|
let note = self.get_note_by_id(id).await?;
|
||||||
|
|
||||||
|
// check user permission
|
||||||
|
if user.id != note.owner && !user.permissions.check(FinePermission::MANAGE_NOTES) {
|
||||||
|
return Err(Error::NotAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = execute!(&conn, "DELETE FROM notes WHERE id = $1", &[&(id as i64)]);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete notes
|
||||||
|
let res = execute!(&conn, "DELETE FROM notes WHERE note = $1", &[&(id as i64)]);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
self.0.1.remove(format!("atto.note:{}", id)).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_method!(update_note_title(&str)@get_note_by_id:MANAGE_NOTES -> "UPDATE notes SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.note:{}");
|
||||||
|
}
|
|
@ -1620,7 +1620,14 @@ impl DataManager {
|
||||||
|
|
||||||
// create notification for question owner
|
// create notification for question owner
|
||||||
// (if the current user isn't the owner)
|
// (if the current user isn't the owner)
|
||||||
if (question.owner != data.owner) && (question.owner != 0) {
|
if (question.owner != data.owner)
|
||||||
|
&& (question.owner != 0)
|
||||||
|
&& (!owner.settings.private_profile
|
||||||
|
| self
|
||||||
|
.get_userfollow_by_initiator_receiver(data.owner, question.owner)
|
||||||
|
.await
|
||||||
|
.is_ok())
|
||||||
|
{
|
||||||
self.create_notification(Notification::new(
|
self.create_notification(Notification::new(
|
||||||
"Your question has received a new answer!".to_string(),
|
"Your question has received a new answer!".to_string(),
|
||||||
format!(
|
format!(
|
||||||
|
|
69
crates/core/src/model/journals.rs
Normal file
69
crates/core/src/model/journals.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum JournalViewPermission {
|
||||||
|
/// Can be accessed by anyone via link.
|
||||||
|
Public,
|
||||||
|
/// Visible only to the journal owner.
|
||||||
|
Private,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for JournalViewPermission {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Private
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Journal {
|
||||||
|
pub id: usize,
|
||||||
|
pub created: usize,
|
||||||
|
pub owner: usize,
|
||||||
|
pub title: String,
|
||||||
|
pub view: JournalViewPermission,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Journal {
|
||||||
|
/// Create a new [`Journal`].
|
||||||
|
pub fn new(owner: usize, title: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||||
|
created: unix_epoch_timestamp(),
|
||||||
|
owner,
|
||||||
|
title,
|
||||||
|
view: JournalViewPermission::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Note {
|
||||||
|
pub id: usize,
|
||||||
|
pub created: usize,
|
||||||
|
pub owner: usize,
|
||||||
|
pub title: String,
|
||||||
|
/// The ID of the [`Journal`] this note belongs to.
|
||||||
|
///
|
||||||
|
/// The note is subject to the settings set for the journal it's in.
|
||||||
|
pub journal: usize,
|
||||||
|
pub content: String,
|
||||||
|
pub edited: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Note {
|
||||||
|
/// Create a new [`Note`].
|
||||||
|
pub fn new(owner: usize, title: String, journal: usize, content: String) -> Self {
|
||||||
|
let created = unix_epoch_timestamp();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||||
|
created,
|
||||||
|
owner,
|
||||||
|
title,
|
||||||
|
journal,
|
||||||
|
content,
|
||||||
|
edited: created,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ pub mod auth;
|
||||||
pub mod channels;
|
pub mod channels;
|
||||||
pub mod communities;
|
pub mod communities;
|
||||||
pub mod communities_permissions;
|
pub mod communities_permissions;
|
||||||
|
pub mod journals;
|
||||||
pub mod moderation;
|
pub mod moderation;
|
||||||
pub mod oauth;
|
pub mod oauth;
|
||||||
pub mod permissions;
|
pub mod permissions;
|
||||||
|
|
|
@ -37,6 +37,8 @@ bitflags! {
|
||||||
const MANAGE_STACKS = 1 << 26;
|
const MANAGE_STACKS = 1 << 26;
|
||||||
const STAFF_BADGE = 1 << 27;
|
const STAFF_BADGE = 1 << 27;
|
||||||
const MANAGE_APPS = 1 << 28;
|
const MANAGE_APPS = 1 << 28;
|
||||||
|
const MANAGE_JOURNALS = 1 << 29;
|
||||||
|
const MANAGE_NOTES = 1 << 30;
|
||||||
|
|
||||||
const _ = !0;
|
const _ = !0;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue