init
This commit is contained in:
Generated
+8
@@ -2227,6 +2227,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"sea-orm-migration",
|
"sea-orm-migration",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3376,6 +3377,12 @@ dependencies = [
|
|||||||
"digest 0.11.3",
|
"digest 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1_smol"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -4242,6 +4249,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
|
"sha1_smol",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -11,10 +11,10 @@ udp_port = 8080
|
|||||||
[database]
|
[database]
|
||||||
# DSN for database
|
# DSN for database
|
||||||
# SQLite
|
# SQLite
|
||||||
url = "sqlite://oxspeak.db"
|
#url = "sqlite://oxspeak.db"
|
||||||
#url = "sqlite::memory:"
|
#url = "sqlite::memory:"
|
||||||
# PostgreSQL
|
# PostgreSQL
|
||||||
# url = "postgresql://user:passwd@localhost:5432/db_name"
|
url = "postgresql://oxspeak:oxspeak@localhost:5432/oxspeak"
|
||||||
# MySQL
|
# MySQL
|
||||||
# url = "mysql://user:passwd@localhost:3306/db_name"
|
# url = "mysql://user:passwd@localhost:3306/db_name"
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ path = "src/lib.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||||
|
uuid = { version = "1", features = ["v5"] }
|
||||||
|
|
||||||
[dependencies.sea-orm-migration]
|
[dependencies.sea-orm-migration]
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,9 @@
|
|||||||
use sea_orm_migration::prelude::*;
|
use sea_orm_migration::prelude::*;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
const UNICODE_EMOJI_VERSION: &str = "17.0";
|
||||||
|
const UNICODE_EMOJI_DATA: &str = include_str!("../assets/emoji-rgi-17.0.txt");
|
||||||
|
|
||||||
#[derive(DeriveMigrationName)]
|
#[derive(DeriveMigrationName)]
|
||||||
pub struct Migration;
|
pub struct Migration;
|
||||||
@@ -698,6 +703,8 @@ impl MigrationTrait for Migration {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
seed_unicode_emojis(manager).await?;
|
||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
Table::create()
|
Table::create()
|
||||||
@@ -746,6 +753,8 @@ impl MigrationTrait for Migration {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
seed_unicode_aliases(manager).await?;
|
||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
Table::create()
|
Table::create()
|
||||||
@@ -1044,3 +1053,187 @@ impl MigrationTrait for Migration {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Seeds the global RGI emoji set from the pinned Unicode 17.0 data file.
|
||||||
|
///
|
||||||
|
/// The inserts are intentionally chunked because SQLite commonly limits the
|
||||||
|
/// number of bind parameters in a single statement to 999.
|
||||||
|
async fn seed_unicode_emojis(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
||||||
|
const CHUNK_SIZE: usize = 250;
|
||||||
|
let rows = unicode_rows()?;
|
||||||
|
|
||||||
|
for chunk in rows.chunks(CHUNK_SIZE) {
|
||||||
|
let mut insert = Query::insert();
|
||||||
|
insert.into_table(Alias::new("emoji")).columns([
|
||||||
|
Alias::new("id"),
|
||||||
|
Alias::new("server_id"),
|
||||||
|
Alias::new("emoji_type"),
|
||||||
|
Alias::new("unicode_sequence"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (id, sequence, _) in chunk {
|
||||||
|
insert.values_panic([
|
||||||
|
Expr::val(*id).into(),
|
||||||
|
Expr::val(Option::<Uuid>::None).into(),
|
||||||
|
Expr::val("unicode").into(),
|
||||||
|
Expr::val(sequence.as_str()).into(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.exec_stmt(insert).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn seed_unicode_aliases(manager: &SchemaManager<'_>) -> Result<(), DbErr> {
|
||||||
|
const CHUNK_SIZE: usize = 250;
|
||||||
|
let rows = unicode_rows()?;
|
||||||
|
let mut aliases = Vec::with_capacity(rows.len());
|
||||||
|
let mut used = HashSet::with_capacity(rows.len());
|
||||||
|
|
||||||
|
for (_, sequence, name) in rows {
|
||||||
|
let base = normalize_unicode_alias(&name);
|
||||||
|
let mut alias = base.clone();
|
||||||
|
if !used.insert(alias.clone()) {
|
||||||
|
alias = format!("{base}_u{}", sequence.replace(' ', ""));
|
||||||
|
if alias.len() > 64 {
|
||||||
|
alias.truncate(64);
|
||||||
|
}
|
||||||
|
if !used.insert(alias.clone()) {
|
||||||
|
return Err(DbErr::Custom(format!(
|
||||||
|
"duplicate generated emoji alias: {alias}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let emoji_id = Uuid::new_v5(
|
||||||
|
&Uuid::NAMESPACE_URL,
|
||||||
|
format!("https://oxspeak.local/unicode/{UNICODE_EMOJI_VERSION}/{sequence}").as_bytes(),
|
||||||
|
);
|
||||||
|
let id = Uuid::new_v5(
|
||||||
|
&Uuid::NAMESPACE_URL,
|
||||||
|
format!("https://oxspeak.local/unicode-alias/{UNICODE_EMOJI_VERSION}/{sequence}")
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
aliases.push((id, emoji_id, alias));
|
||||||
|
}
|
||||||
|
|
||||||
|
for chunk in aliases.chunks(CHUNK_SIZE) {
|
||||||
|
let mut insert = Query::insert();
|
||||||
|
insert.into_table(Alias::new("emoji_alias")).columns([
|
||||||
|
Alias::new("id"),
|
||||||
|
Alias::new("emoji_id"),
|
||||||
|
Alias::new("alias"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (id, emoji_id, alias) in chunk {
|
||||||
|
insert.values_panic([
|
||||||
|
Expr::val(*id).into(),
|
||||||
|
Expr::val(*emoji_id).into(),
|
||||||
|
Expr::val(alias.as_str()).into(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.exec_stmt(insert).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unicode_rows() -> Result<Vec<(Uuid, String, String)>, DbErr> {
|
||||||
|
UNICODE_EMOJI_DATA
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.trim().is_empty())
|
||||||
|
.map(|line| {
|
||||||
|
let (codepoints, name) = line.split_once('\t').ok_or_else(|| {
|
||||||
|
DbErr::Custom(format!("invalid Unicode emoji data line: {line:?}"))
|
||||||
|
})?;
|
||||||
|
let sequence = parse_unicode_sequence(codepoints)?;
|
||||||
|
let id = Uuid::new_v5(
|
||||||
|
&Uuid::NAMESPACE_URL,
|
||||||
|
format!("https://oxspeak.local/unicode/{UNICODE_EMOJI_VERSION}/{sequence}")
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
Ok((id, sequence, name.to_string()))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_unicode_alias(name: &str) -> String {
|
||||||
|
let mut alias = String::with_capacity(name.len());
|
||||||
|
let mut separator = false;
|
||||||
|
|
||||||
|
for character in name.chars() {
|
||||||
|
if character.is_ascii_alphanumeric() {
|
||||||
|
if separator && !alias.is_empty() {
|
||||||
|
alias.push('_');
|
||||||
|
}
|
||||||
|
alias.push(character.to_ascii_lowercase());
|
||||||
|
separator = false;
|
||||||
|
} else {
|
||||||
|
separator = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alias.truncate(64);
|
||||||
|
alias.trim_end_matches('_').to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_unicode_sequence(codepoints: &str) -> Result<String, DbErr> {
|
||||||
|
codepoints
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|codepoint| {
|
||||||
|
let value = u32::from_str_radix(codepoint, 16).map_err(|error| {
|
||||||
|
DbErr::Custom(format!("invalid Unicode codepoint {codepoint:?}: {error}"))
|
||||||
|
})?;
|
||||||
|
char::from_u32(value).ok_or_else(|| {
|
||||||
|
DbErr::Custom(format!("invalid Unicode scalar value: {codepoint:?}"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unicode_dataset_contains_the_pinned_rgi_set() {
|
||||||
|
let entries: Vec<_> = unicode_rows().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(entries.len(), 3944);
|
||||||
|
assert!(entries.iter().any(|(_, sequence, name)| {
|
||||||
|
sequence == "🇺🇸" && name == "flag: United States"
|
||||||
|
}));
|
||||||
|
assert!(entries.iter().any(|(_, sequence, name)| {
|
||||||
|
sequence == "👍🏽" && name == "thumbs up: medium skin tone"
|
||||||
|
}));
|
||||||
|
assert!(entries.iter().any(|(_, sequence, name)| {
|
||||||
|
sequence == "👩💻" && name == "woman technologist"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unicode_sequence_parser_handles_sequences_and_rejects_invalid_values() {
|
||||||
|
assert_eq!(parse_unicode_sequence("1F1FA 1F1F8").unwrap(), "🇺🇸");
|
||||||
|
assert_eq!(parse_unicode_sequence("1F44D 1F3FD").unwrap(), "👍🏽");
|
||||||
|
assert!(parse_unicode_sequence("not-a-codepoint").is_err());
|
||||||
|
assert!(parse_unicode_sequence("110000").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unicode_aliases_are_normalized_for_the_existing_api_constraints() {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_unicode_alias("flag: United States"),
|
||||||
|
"flag_united_states"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
normalize_unicode_alias("thumbs up: medium skin tone"),
|
||||||
|
"thumbs_up_medium_skin_tone"
|
||||||
|
);
|
||||||
|
assert!(normalize_unicode_alias("woman technologist")
|
||||||
|
.chars()
|
||||||
|
.all(|character| character.is_ascii_alphanumeric() || character == '_'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,7 +124,10 @@ impl EmojiService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub fn hash(data: &[u8]) -> String {
|
pub fn hash(data: &[u8]) -> String {
|
||||||
format!("{:x}", Sha256::digest(data))
|
Sha256::digest(data)
|
||||||
|
.iter()
|
||||||
|
.map(|byte| format!("{byte:02x}"))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
pub async fn save_asset(
|
pub async fn save_asset(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
|
|||||||
Reference in New Issue
Block a user